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                    // v7.40.12 — PG's two-word spelling of the
3036                    // `timezone` GUC, and the one psql's own `\timing`
3037                    // era documentation and every ORM's dialect probe
3038                    // send. Until now only the pgwire host recognised
3039                    // it, in a shortcut that answered SHOW from a copy;
3040                    // removing that shortcut moved the spelling here,
3041                    // where the embedded API gets it too. Same shape as
3042                    // the isolation-level arm above: `time` is a bare
3043                    // ident, so without this the statement ended there
3044                    // and `ZONE` was a syntax error.
3045                    "time"
3046                        if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("zone")) =>
3047                    {
3048                        self.advance(); // ZONE
3049                        Ok(Statement::ShowParameter("timezone".to_string()))
3050                    }
3051                    // v7.40.12 — `SHOW SESSION AUTHORIZATION`, PG's
3052                    // spelling of the login identity. `session` is a bare
3053                    // ident, so the statement ended there and `AUTHORIZATION`
3054                    // was a syntax error; the engine already knows the
3055                    // answer, as `SELECT session_user` on the same
3056                    // connection shows.
3057                    "session"
3058                        if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("authorization")) =>
3059                    {
3060                        self.advance(); // AUTHORIZATION
3061                        Ok(Statement::ShowParameter(
3062                            "session_authorization".to_string(),
3063                        ))
3064                    }
3065                    other => {
3066                        // v7.38 (read01 P3.20) — a custom namespaced GUC
3067                        // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
3068                        // consume the dotted tail so it round-trips with
3069                        // `SET app.foo` / `current_setting('app.foo')`.
3070                        let mut full = other.to_string();
3071                        while matches!(self.peek(), Token::Dot) {
3072                            self.advance();
3073                            let seg = self.expect_ident_like()?;
3074                            full.push('.');
3075                            full.push_str(&seg.to_ascii_lowercase());
3076                        }
3077                        Ok(Statement::ShowParameter(full))
3078                    }
3079                }
3080            }
3081            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
3082            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
3083            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
3084            // arrived as a bare ident; tokenising it dedicatedly
3085            // keeps the dispatch tree small.
3086            Token::Drop => {
3087                self.advance();
3088                match self.peek() {
3089                    // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
3090                    // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
3091                    // around DROP ROLE cleanup. SPG has no role-owner
3092                    // model, so consume to boundary as a no-op.
3093                    Token::Ident(s) | Token::QuotedIdent(s)
3094                        if s.eq_ignore_ascii_case("owned") =>
3095                    {
3096                        // v7.39 (round 696) — still a no-op (SPG has no
3097                        // role-owner model), but the ROLE is carried out so
3098                        // the engine can refuse one that does not exist,
3099                        // which is what PG18 does.
3100                        self.advance();
3101                        if self.peek_is_by() {
3102                            self.advance();
3103                        }
3104                        let names = self.take_comma_separated_names();
3105                        self.consume_until_statement_boundary();
3106                        Ok(Statement::ValidateOnly {
3107                            kind: crate::ast::ValidateOnlyKind::RoleName,
3108                            names,
3109                        })
3110                    }
3111                    // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
3112                    // It drops only a TEMPORARY table, and name resolution
3113                    // already prefers the session's own, so the keyword is
3114                    // consumed and the ordinary DROP TABLE path runs.
3115                    Token::Ident(s) | Token::QuotedIdent(s)
3116                        if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
3117                    {
3118                        self.advance();
3119                        if !matches!(self.peek(), Token::Table) {
3120                            return Err(self.err(alloc::format!(
3121                                "expected TABLE after DROP TEMPORARY, got {:?}",
3122                                self.peek()
3123                            )));
3124                        }
3125                        self.parse_drop_table_after_keyword()
3126                    }
3127                    Token::Publication => {
3128                        self.advance();
3129                        // v7.39 (round 754, F31-B4) — the round-753
3130                        // audit probe tripped over the missing
3131                        // `IF EXISTS` here (syntax error).
3132                        let if_exists = self.consume_if_exists();
3133                        let name = self.expect_ident_or_string()?;
3134                        Ok(Statement::DropPublication { name, if_exists })
3135                    }
3136                    Token::Subscription => {
3137                        self.advance();
3138                        let if_exists = self.consume_if_exists();
3139                        let name = self.expect_ident_or_string()?;
3140                        Ok(Statement::DropSubscription { name, if_exists })
3141                    }
3142                    Token::Ident(s) | Token::QuotedIdent(s)
3143                        if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3144                    {
3145                        self.advance();
3146                        // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3147                        // login user IS a role in PG, and SPG's store holds
3148                        // both. `IF EXISTS` is accepted on either spelling.
3149                        let if_exists = self.consume_if_exists();
3150                        let name = self.expect_ident_or_string()?;
3151                        Ok(Statement::DropUser { name, if_exists })
3152                    }
3153                    // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3154                    // CREATE DATABASE has parsed since v7.14 and this did
3155                    // not, so `DROP DATABASE IF EXISTS x` — what every
3156                    // teardown script and pg_dumpall preamble opens with —
3157                    // came back as a syntax error, which IF EXISTS cannot
3158                    // soften. The name is carried so the engine can answer
3159                    // the way PG does; PG never lets this succeed on a
3160                    // single-database server, since the name is either
3161                    // unknown ("database … does not exist", or a notice
3162                    // under IF EXISTS) or the one you are connected to
3163                    // ("cannot drop the currently open database").
3164                    Token::Ident(s) | Token::QuotedIdent(s)
3165                        if s.eq_ignore_ascii_case("database") =>
3166                    {
3167                        self.advance();
3168                        let if_exists = self.consume_if_exists();
3169                        let name = self.expect_ident_or_string()?;
3170                        self.consume_until_statement_boundary();
3171                        Ok(Statement::DropDatabase { name, if_exists })
3172                    }
3173                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3174                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3175                        self.advance();
3176                        let if_exists = self.consume_if_exists();
3177                        let name = self.expect_ident_like()?;
3178                        // ON <table>
3179                        if !matches!(self.peek(), Token::On) {
3180                            return Err(self.err(alloc::format!(
3181                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3182                                self.peek()
3183                            )));
3184                        }
3185                        self.advance();
3186                        let table = self.expect_ident_like()?;
3187                        Ok(Statement::DropTrigger {
3188                            name,
3189                            table,
3190                            if_exists,
3191                        })
3192                    }
3193                    // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3194                    // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3195                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3196                        self.advance();
3197                        let if_exists = self.consume_if_exists();
3198                        let name = self.expect_ident_like()?;
3199                        if !matches!(self.peek(), Token::On) {
3200                            return Err(self.err(alloc::format!(
3201                                "expected ON <table> after DROP RULE {name:?}, got {:?}",
3202                                self.peek()
3203                            )));
3204                        }
3205                        self.advance();
3206                        let table = self.expect_ident_like()?;
3207                        // Optional CASCADE / RESTRICT — accepted, no effect.
3208                        self.consume_until_statement_boundary();
3209                        Ok(Statement::DropRule {
3210                            name,
3211                            table,
3212                            if_exists,
3213                        })
3214                    }
3215                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3216                    // v7.12.4 ignores any optional arg-list (signature-
3217                    // based overload disambiguation lands in v7.12.5+).
3218                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3219                        self.advance();
3220                        let if_exists = self.consume_if_exists();
3221                        let name = self.expect_ident_like()?;
3222                        // v7.39 (read01 round 62) — the argument list identifies
3223                        // WHICH overload to drop, so it is captured, not
3224                        // discarded. `DROP FUNCTION f` (no list) is legal when
3225                        // the name is unambiguous; the engine enforces that.
3226                        let args = if matches!(self.peek(), Token::LParen) {
3227                            Some(self.parse_function_signature_types()?)
3228                        } else {
3229                            None
3230                        };
3231                        // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3232                        // trailer, which `DROP TABLE` and `DROP INDEX` have
3233                        // accepted since v7.14 and this one refused outright.
3234                        // pg_dump writes it, so refusing was a parse error in
3235                        // the middle of a restore. SPG drops the function
3236                        // either way — it tracks no dependents to cascade to —
3237                        // which is the same reading the other two give it.
3238                        self.consume_drop_behaviour();
3239                        Ok(Statement::DropFunction {
3240                            name,
3241                            args,
3242                            if_exists,
3243                        })
3244                    }
3245                    // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3246                    // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3247                    // emit DROP TABLE IF EXISTS at the head of every
3248                    // CREATE TABLE block so re-importing a dump
3249                    // overwrites prior state. SPG accepts and removes
3250                    // matching tables; CASCADE/RESTRICT trailers
3251                    // accepted silently.
3252                    Token::Table => self.parse_drop_table_after_keyword(),
3253                    // v7.14.0 — DROP INDEX [IF EXISTS] name
3254                    // [CASCADE|RESTRICT]. PG / mysqldump emit this
3255                    // for partial-index renames and pgvector
3256                    // migrations. SPG removes the matching index;
3257                    // IF EXISTS makes the drop idempotent.
3258                    Token::Index => {
3259                        self.advance();
3260                        let if_exists_at = self.pos;
3261                        let if_exists = self.consume_if_exists();
3262                        let name = self.expect_ident_like()?;
3263                        // v7.39.7 — MySQL's own spelling, which SPG
3264                        // refused.
3265                        //
3266                        // `DROP INDEX i ON t` is how MySQL drops an
3267                        // index; its names live inside a table, so the
3268                        // statement names the table. Measured against
3269                        // MySQL 9.7.2: the form above works, and the
3270                        // bare `DROP INDEX i` PostgreSQL uses is a 1064
3271                        // there. SPG had it exactly backwards on the
3272                        // MySQL wire — the bare form accepted, MySQL's
3273                        // own a syntax error — so a migration that drops
3274                        // an index failed against the drop-in and not
3275                        // against the thing it replaces.
3276                        let table = if matches!(self.peek(), Token::On) {
3277                            self.advance();
3278                            Some(self.expect_ident_like()?)
3279                        } else {
3280                            None
3281                        };
3282                        if self.mysql_dialect {
3283                            // MySQL has no `IF EXISTS` here either:
3284                            // `DROP INDEX IF EXISTS i ON t` is a 1064.
3285                            if if_exists {
3286                                return Err(self.err_at(
3287                                    if_exists_at,
3288                                    "MySQL has no IF EXISTS on DROP INDEX".into(),
3289                                ));
3290                            }
3291                            if table.is_none() {
3292                                return Err(self.err("expected ON after the index name".into()));
3293                            }
3294                        }
3295                        if matches!(
3296                            self.peek(),
3297                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3298                                || s.eq_ignore_ascii_case("restrict")
3299                        ) {
3300                            self.advance();
3301                        }
3302                        Ok(Statement::DropIndex {
3303                            name,
3304                            if_exists,
3305                            table,
3306                        })
3307                    }
3308                    // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3309                    // [CASCADE|RESTRICT]. SPG is single-database;
3310                    // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3311                    // name [, name…] [CASCADE | RESTRICT]. Real
3312                    // unregister (was silent no-op pre-v7.17).
3313                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3314                        self.advance();
3315                        let if_exists = self.consume_if_exists();
3316                        let mut names = vec![self.expect_ident_like()?];
3317                        while matches!(self.peek(), Token::Comma) {
3318                            self.advance();
3319                            names.push(self.expect_ident_like()?);
3320                        }
3321                        if matches!(
3322                            self.peek(),
3323                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3324                                || s.eq_ignore_ascii_case("restrict")
3325                        ) {
3326                            self.advance();
3327                        }
3328                        Ok(Statement::DropSchema { names, if_exists })
3329                    }
3330                    // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3331                    // name [, name…] [CASCADE|RESTRICT].
3332                    Token::Ident(s) | Token::QuotedIdent(s)
3333                        if s.eq_ignore_ascii_case("type") =>
3334                    {
3335                        self.advance();
3336                        let if_exists = self.consume_if_exists();
3337                        let mut names = vec![self.expect_ident_like()?];
3338                        while matches!(self.peek(), Token::Comma) {
3339                            self.advance();
3340                            names.push(self.expect_ident_like()?);
3341                        }
3342                        if matches!(
3343                            self.peek(),
3344                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3345                                || s.eq_ignore_ascii_case("restrict")
3346                        ) {
3347                            self.advance();
3348                        }
3349                        Ok(Statement::DropType { names, if_exists })
3350                    }
3351                    // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3352                    // name [, name…] [CASCADE|RESTRICT].
3353                    Token::Ident(s) | Token::QuotedIdent(s)
3354                        if s.eq_ignore_ascii_case("domain") =>
3355                    {
3356                        self.advance();
3357                        let if_exists = self.consume_if_exists();
3358                        let mut names = vec![self.expect_ident_like()?];
3359                        while matches!(self.peek(), Token::Comma) {
3360                            self.advance();
3361                            names.push(self.expect_ident_like()?);
3362                        }
3363                        if matches!(
3364                            self.peek(),
3365                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3366                                || s.eq_ignore_ascii_case("restrict")
3367                        ) {
3368                            self.advance();
3369                        }
3370                        Ok(Statement::DropDomain { names, if_exists })
3371                    }
3372                    // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3373                    // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3374                    Token::Ident(s) | Token::QuotedIdent(s)
3375                        if s.eq_ignore_ascii_case("materialized") =>
3376                    {
3377                        self.advance();
3378                        let nxt = self.peek().clone();
3379                        if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3380                        {
3381                            return Err(self.err(alloc::format!(
3382                                "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3383                            )));
3384                        }
3385                        self.advance();
3386                        let if_exists = self.consume_if_exists();
3387                        let mut names = vec![self.expect_ident_like()?];
3388                        while matches!(self.peek(), Token::Comma) {
3389                            self.advance();
3390                            names.push(self.expect_ident_like()?);
3391                        }
3392                        if matches!(
3393                            self.peek(),
3394                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3395                                || s.eq_ignore_ascii_case("restrict")
3396                        ) {
3397                            self.advance();
3398                        }
3399                        Ok(Statement::DropMaterializedView { names, if_exists })
3400                    }
3401                    // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3402                    // name [, name…] [CASCADE|RESTRICT].
3403                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3404                        self.advance();
3405                        let if_exists = self.consume_if_exists();
3406                        let mut names = vec![self.expect_ident_like()?];
3407                        while matches!(self.peek(), Token::Comma) {
3408                            self.advance();
3409                            names.push(self.expect_ident_like()?);
3410                        }
3411                        if matches!(
3412                            self.peek(),
3413                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3414                                || s.eq_ignore_ascii_case("restrict")
3415                        ) {
3416                            self.advance();
3417                        }
3418                        Ok(Statement::DropView { names, if_exists })
3419                    }
3420                    // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3421                    // [CASCADE|RESTRICT]. Real removal from catalog
3422                    // (was a silent no-op pre-v7.17).
3423                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3424                        self.advance();
3425                        let if_exists = self.consume_if_exists();
3426                        let mut names = vec![self.expect_ident_like()?];
3427                        while matches!(self.peek(), Token::Comma) {
3428                            self.advance();
3429                            names.push(self.expect_ident_like()?);
3430                        }
3431                        if matches!(
3432                            self.peek(),
3433                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3434                                || s.eq_ignore_ascii_case("restrict")
3435                        ) {
3436                            self.advance();
3437                        }
3438                        Ok(Statement::DropSequence { names, if_exists })
3439                    }
3440                    // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3441                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3442                        self.advance();
3443                        self.parse_drop_policy_after_keyword()
3444                    }
3445                    // v7.37.17 (17.6 siblings) — DROP <target> for
3446                    // targets SPG doesn't natively track. pg_dump
3447                    // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3448                    // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3449                    // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3450                    // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3451                    // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3452                    // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3453                    // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3454                    // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3455                    // etc. — accept + Empty-return so pg_dump tails
3456                    // load through. Materialized-view drop dispatches
3457                    // to the existing DropTable path when the token
3458                    // is Materialized-View-shaped (elsewhere in
3459                    // this parser).
3460                    Token::Ident(s) | Token::QuotedIdent(s)
3461                        if s.eq_ignore_ascii_case("text")
3462                            // The DROP dispatch matches on PEEK — `text` is
3463                            // not yet consumed, so SEARCH/CONFIGURATION sit
3464                            // at pos+1/pos+2 (the round-695 trap's mirror).
3465                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3466                            && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3467                    {
3468                        // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3469                        // validates the name; DICTIONARY / PARSER / TEMPLATE
3470                        // stay in the noise arm below.
3471                        self.advance(); // TEXT
3472                        self.advance(); // SEARCH
3473                        self.advance(); // CONFIGURATION
3474                        let if_exists = self.consume_if_exists();
3475                        let names = self.take_comma_separated_names();
3476                        self.consume_until_statement_boundary();
3477                        if if_exists {
3478                            return Ok(Statement::Empty);
3479                        }
3480                        Ok(Statement::ValidateOnly {
3481                            kind: crate::ast::ValidateOnlyKind::TsConfigName,
3482                            names,
3483                        })
3484                    }
3485                    Token::Ident(s) | Token::QuotedIdent(s)
3486                        if matches!(
3487                            s.to_ascii_lowercase().as_str(),
3488                            "type"
3489                                | "domain"
3490                                | "operator"
3491                                | "cast"
3492                                // `text` = TEXT SEARCH DICTIONARY / PARSER /
3493                                // TEMPLATE (CONFIGURATION intercepted above).
3494                                | "text"
3495                                | "materialized"
3496                                | "large"
3497                                | "role"
3498                                | "access"
3499                                | "procedure"
3500                                | "routine"
3501                        ) =>
3502                    {
3503                        self.consume_until_statement_boundary();
3504                        Ok(Statement::Empty)
3505                    }
3506                    // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3507                    // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3508                    // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3509                    // foreign-data warning family (round 706) so a
3510                    // CREATE→DROP sequence in a dump stays consistent.
3511                    Token::Ident(s) | Token::QuotedIdent(s)
3512                        if s.eq_ignore_ascii_case("server")
3513                            || s.eq_ignore_ascii_case("foreign") =>
3514                    {
3515                        self.advance();
3516                        self.consume_until_statement_boundary();
3517                        Ok(Statement::ValidateOnly {
3518                            kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3519                            names: Vec::new(),
3520                        })
3521                    }
3522                    Token::Ident(s) | Token::QuotedIdent(s)
3523                        if s.eq_ignore_ascii_case("collation")
3524                            || s.eq_ignore_ascii_case("tablespace") =>
3525                    {
3526                        let kind = if s.eq_ignore_ascii_case("collation") {
3527                            crate::ast::ValidateOnlyKind::CollationName
3528                        } else {
3529                            crate::ast::ValidateOnlyKind::TablespaceName
3530                        };
3531                        self.advance();
3532                        let if_exists = self.consume_if_exists();
3533                        let names = self.take_comma_separated_names();
3534                        self.consume_until_statement_boundary();
3535                        if if_exists {
3536                            return Ok(Statement::Empty);
3537                        }
3538                        Ok(Statement::ValidateOnly { kind, names })
3539                    }
3540                    Token::Ident(s) | Token::QuotedIdent(s)
3541                        if s.eq_ignore_ascii_case("event") =>
3542                    {
3543                        self.advance();
3544                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
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 {
3555                            kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3556                            names,
3557                        })
3558                    }
3559                    // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3560                    // leave the noise list; see the ValidateOnly kinds.
3561                    Token::Ident(s) | Token::QuotedIdent(s)
3562                        if s.eq_ignore_ascii_case("conversion")
3563                            || s.eq_ignore_ascii_case("language")
3564                            // `DROP PROCEDURAL LANGUAGE` puts the modifier
3565                            // FIRST — the first draft looked for it after.
3566                            || s.eq_ignore_ascii_case("procedural") =>
3567                    {
3568                        let kind = if s.eq_ignore_ascii_case("conversion") {
3569                            crate::ast::ValidateOnlyKind::ConversionName
3570                        } else {
3571                            crate::ast::ValidateOnlyKind::LanguageName
3572                        };
3573                        self.advance();
3574                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3575                        {
3576                            self.advance();
3577                        }
3578                        let if_exists = self.consume_if_exists();
3579                        let names = self.take_comma_separated_names();
3580                        self.consume_until_statement_boundary();
3581                        if if_exists {
3582                            return Ok(Statement::Empty);
3583                        }
3584                        Ok(Statement::ValidateOnly { kind, names })
3585                    }
3586                    // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3587                    // name(argtypes)[, …]`. Parsed for real so the engine
3588                    // can answer as PG does; see Statement::DropAggregate.
3589                    Token::Ident(s) | Token::QuotedIdent(s)
3590                        if s.eq_ignore_ascii_case("aggregate") =>
3591                    {
3592                        self.advance();
3593                        let if_exists = self.consume_if_exists();
3594                        let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3595                        loop {
3596                            let name = self.expect_ident_like()?;
3597                            if !matches!(self.peek(), Token::LParen) {
3598                                return Err(self.err(alloc::format!(
3599                                    "expected argument list after DROP AGGREGATE {name}"
3600                                )));
3601                            }
3602                            self.advance();
3603                            let mut args: Vec<String> = Vec::new();
3604                            let mut star = false;
3605                            loop {
3606                                match self.peek().clone() {
3607                                    Token::RParen => {
3608                                        self.advance();
3609                                        break;
3610                                    }
3611                                    Token::Star => {
3612                                        self.advance();
3613                                        star = true;
3614                                    }
3615                                    Token::Comma => {
3616                                        self.advance();
3617                                    }
3618                                    _ => {
3619                                        // A type name may be multi-token
3620                                        // (`double precision`); glue idents
3621                                        // until , or ).
3622                                        let mut t = self.expect_ident_like()?;
3623                                        while let Token::Ident(nx) = self.peek() {
3624                                            let nx = nx.clone();
3625                                            self.advance();
3626                                            t.push(' ');
3627                                            t.push_str(&nx);
3628                                        }
3629                                        args.push(t);
3630                                    }
3631                                }
3632                            }
3633                            items.push((name, if star { None } else { Some(args) }));
3634                            if matches!(self.peek(), Token::Comma) {
3635                                self.advance();
3636                            } else {
3637                                break;
3638                            }
3639                        }
3640                        self.consume_until_statement_boundary();
3641                        Ok(Statement::DropAggregate { if_exists, items })
3642                    }
3643                    // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3644                    // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3645                    // installed; `IF EXISTS` is the spelling that says do
3646                    // not, and it keeps the no-op.
3647                    Token::Ident(s) | Token::QuotedIdent(s)
3648                        if s.eq_ignore_ascii_case("extension") =>
3649                    {
3650                        self.advance();
3651                        let if_exists = self.consume_if_exists();
3652                        let names = self.take_comma_separated_names();
3653                        self.consume_until_statement_boundary();
3654                        if if_exists {
3655                            return Ok(Statement::Empty);
3656                        }
3657                        Ok(Statement::ValidateOnly {
3658                            kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3659                            names,
3660                        })
3661                    }
3662                    Token::Ident(s) | Token::QuotedIdent(s)
3663                        if s.eq_ignore_ascii_case("statistics") =>
3664                    {
3665                        self.parse_drop_statistics_after_drop()
3666                    }
3667                    other => Err(self.err(format!(
3668                        "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3669                         SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3670                    ))),
3671                }
3672            }
3673            // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3674            // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3675            // and accepted before the view name. SPG materialised
3676            // views re-evaluate on read (always-fresh semantics), so
3677            // the CONCURRENTLY-vs-serial distinction has no runtime
3678            // effect — the refresh body does not block readers either
3679            // way. Same accept-and-no-op pattern as DETACH PARTITION
3680            // CONCURRENTLY (16.5).
3681            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3682                self.advance();
3683                let nxt = self.peek().clone();
3684                if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3685                {
3686                    return Err(self.err(alloc::format!(
3687                        "expected MATERIALIZED after REFRESH, got {nxt:?}"
3688                    )));
3689                }
3690                self.advance();
3691                let nxt2 = self.peek().clone();
3692                if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3693                {
3694                    return Err(self.err(alloc::format!(
3695                        "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3696                    )));
3697                }
3698                self.advance();
3699                // Optional CONCURRENTLY noise word — consumed without
3700                // changing semantics.
3701                if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3702                {
3703                    self.advance();
3704                }
3705                let name = self.expect_ident_like()?;
3706                let with_data = self.parse_optional_with_data(true)?;
3707                Ok(Statement::RefreshMaterializedView { name, with_data })
3708            }
3709            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3710                self.advance();
3711                self.parse_update_after_keyword()
3712            }
3713            // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3714            // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3715            // [CASCADE | RESTRICT]. Clears every row from each named
3716            // table. Parses at the top level; the engine dispatcher
3717            // walks Statement::Truncate.
3718            // v7.39.9 — MySQL's top-level `RENAME TABLE a TO b [, c TO d]`.
3719            //
3720            // PostgreSQL renames a table through `ALTER TABLE … RENAME
3721            // TO`, which SPG already had, so this spelling answered 1064
3722            // — and it is what a MySQL migration writes. Measured on
3723            // 9.7.2: several pairs in one statement are accepted, and
3724            // renaming onto a name that exists is 1050.
3725            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename") => {
3726                self.advance();
3727                if matches!(self.peek(), Token::Table) {
3728                    self.advance();
3729                }
3730                let mut pairs: Vec<(String, String)> = Vec::new();
3731                loop {
3732                    let from = self.expect_ident_like()?;
3733                    if matches!(self.peek(), Token::To) {
3734                        self.advance();
3735                    } else {
3736                        self.expect_keyword_ident("to")?;
3737                    }
3738                    let to = self.expect_ident_like()?;
3739                    pairs.push((from, to));
3740                    if matches!(self.peek(), Token::Comma) {
3741                        self.advance();
3742                    } else {
3743                        break;
3744                    }
3745                }
3746                Ok(Statement::RenameTables(pairs))
3747            }
3748            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3749                self.advance();
3750                // Optional TABLE noise word — PG accepts both the reserved
3751                // token and the bare identifier spelling.
3752                if matches!(self.peek(), Token::Table)
3753                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3754                {
3755                    self.advance();
3756                }
3757                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3758                // not absorbed. The lookahead keeps a table genuinely
3759                // called `only` working: the keyword is a keyword only
3760                // when a name follows it.
3761                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3762                    if s.eq_ignore_ascii_case("only"))
3763                    && matches!(
3764                        self.tokens.get(self.pos + 1),
3765                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3766                    );
3767                if only {
3768                    self.advance();
3769                }
3770                // Table names (comma-separated).
3771                let mut tables = Vec::new();
3772                loop {
3773                    tables.push(self.expect_ident_like()?);
3774                    if matches!(self.peek(), Token::Comma) {
3775                        self.advance();
3776                        continue;
3777                    }
3778                    break;
3779                }
3780                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3781                let mut restart_identity = false;
3782                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3783                {
3784                    self.advance();
3785                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3786                    {
3787                        self.advance();
3788                        restart_identity = true;
3789                    }
3790                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3791                {
3792                    self.advance();
3793                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3794                    {
3795                        self.advance();
3796                    }
3797                }
3798                // Optional CASCADE / RESTRICT.
3799                let mut cascade = false;
3800                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3801                {
3802                    self.advance();
3803                    cascade = true;
3804                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3805                {
3806                    self.advance();
3807                }
3808                Ok(Statement::Truncate {
3809                    tables,
3810                    restart_identity,
3811                    cascade,
3812                    only,
3813                })
3814            }
3815            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3816            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3817            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3818            // rows change so the index tree is always up-to-date;
3819            // REINDEX is a strict no-op. Accept the whole statement
3820            // shape to boundary for pg_dump round-trip compatibility.
3821            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3822                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3823                // index bloat to rebuild, so the work stays a no-op, but PG
3824                // validates what it was pointed at and this swallowed the
3825                // name at parse time — `REINDEX TABLE typo` reported
3826                // success. Measured on PG18: INDEX / TABLE name a relation,
3827                // SCHEMA a schema, SYSTEM nothing.
3828                self.advance();
3829                self.parse_reindex_tail()
3830            }
3831            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3832            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3833            // SPG has no MVCC bloat today (Phase D visibility map
3834            // queues with v7.38); the freezer collapses hot-tier
3835            // rows into cold segments automatically. VACUUM is a
3836            // no-op — pg_dump maintenance scripts and Discourse's
3837            // periodic-maintenance path both emit it.
3838            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3839            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3840            // actual bloat, so the pre-MVCC accept-and-ignore posture
3841            // became a silent no-op on a customer's manual reclaim.
3842            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3843            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3844            // ANALYZE is captured, the optional table name is captured.
3845            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3846                self.advance();
3847                // Parenthesised option list: absorb it.
3848                if matches!(self.peek(), Token::LParen) {
3849                    let mut depth = 0usize;
3850                    loop {
3851                        match self.advance() {
3852                            Token::LParen => depth += 1,
3853                            Token::RParen => {
3854                                depth -= 1;
3855                                if depth == 0 {
3856                                    break;
3857                                }
3858                            }
3859                            Token::Eof => break,
3860                            _ => {}
3861                        }
3862                    }
3863                }
3864                let mut analyze = false;
3865                let mut table: Option<String> = None;
3866                loop {
3867                    match self.peek() {
3868                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3869                        // an identifier, so the loop below broke out on it and
3870                        // dropped the table name: `VACUUM FULL nosuch` was
3871                        // accepted where `VACUUM nosuch` was refused.
3872                        Token::Full => {
3873                            self.advance();
3874                        }
3875                        Token::Ident(w) | Token::QuotedIdent(w) => {
3876                            let wl = w.to_ascii_lowercase();
3877                            match wl.as_str() {
3878                                "full" | "freeze" | "verbose" => {
3879                                    self.advance();
3880                                }
3881                                "analyze" | "analyse" => {
3882                                    analyze = true;
3883                                    self.advance();
3884                                }
3885                                _ => {
3886                                    table = Some(self.expect_ident_like()?);
3887                                    break;
3888                                }
3889                            }
3890                        }
3891                        _ => break,
3892                    }
3893                }
3894                // Optional trailing column list / anything else to the
3895                // statement boundary (PG accepts per-column ANALYZE).
3896                self.consume_until_statement_boundary();
3897                Ok(Statement::Vacuum { table, analyze })
3898            }
3899            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3900            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3901            // <index>. PG stores rows in physical order matching
3902            // an index; SPG's hot-tier is append-only + cold-tier
3903            // is segment-frozen, so clustering has no persistent
3904            // effect. Accept-and-no-op for pg_dump compat.
3905            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3906                // v7.39 (round 535) — same as REINDEX above: the relation is
3907                // carried so the engine can refuse one that does not exist.
3908                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3909                self.advance();
3910                self.parse_cluster_tail()
3911            }
3912            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3913            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3914            // optional string payload; UNLISTEN takes a channel or `*`.
3915            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3916                self.advance();
3917                let ch = match self.advance() {
3918                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3919                    other => {
3920                        return Err(self.err(format!(
3921                            "expected channel name after LISTEN, got {other:?}"
3922                        )));
3923                    }
3924                };
3925                Ok(Statement::Listen(ch))
3926            }
3927            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3928                self.advance();
3929                let channel = match self.advance() {
3930                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3931                    other => {
3932                        return Err(self.err(format!(
3933                            "expected channel name after NOTIFY, got {other:?}"
3934                        )));
3935                    }
3936                };
3937                let payload = if matches!(self.peek(), Token::Comma) {
3938                    self.advance();
3939                    match self.advance() {
3940                        Token::String(p) => Some(p),
3941                        other => {
3942                            return Err(self.err(format!(
3943                                "expected string payload after NOTIFY <channel>, got {other:?}"
3944                            )));
3945                        }
3946                    }
3947                } else {
3948                    None
3949                };
3950                Ok(Statement::Notify { channel, payload })
3951            }
3952            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3953                self.advance();
3954                match self.advance() {
3955                    Token::Star => Ok(Statement::Unlisten(None)),
3956                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3957                    other => Err(self.err(format!(
3958                        "expected channel name or * after UNLISTEN, got {other:?}"
3959                    ))),
3960                }
3961            }
3962            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3963            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3964            // process-wide write lock today; explicit LOCK has no
3965            // effect. Accept-and-no-op for pg_dump / migration
3966            // compat.
3967            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3968                self.advance();
3969                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3970                // engine holds a process-wide write lock), but the TABLE
3971                // NAME is now carried out so the engine can refuse one that
3972                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3973                // READ|WRITE` is a different statement with the same first
3974                // word; it keeps the old no-op, because a MySQL dump's
3975                // bracket names tables it is about to create.
3976                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3977                    if k.eq_ignore_ascii_case("tables"));
3978                if mysql_tables {
3979                    self.consume_until_statement_boundary();
3980                    return Ok(Statement::Empty);
3981                }
3982                if matches!(self.peek(), Token::Table) {
3983                    self.advance();
3984                }
3985                let names = self.take_comma_separated_names();
3986                self.consume_until_statement_boundary();
3987                Ok(Statement::ValidateOnly {
3988                    kind: crate::ast::ValidateOnlyKind::LockTable,
3989                    names,
3990                })
3991            }
3992            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3993            // durability marker + snapshot in PG. SPG has WAL
3994            // checkpointing on a byte / time schedule (v7.37.10
3995            // 60s / 4 MiB defaults). The bare statement parses to
3996            // `Statement::Empty` here (the no_std engine owns no
3997            // WAL / snapshot); v7.37 Epic Du wires the HOST
3998            // (embedded `Database::execute_buffered`, via
3999            // `sql_is_checkpoint`) to force an immediate synchronous
4000            // checkpoint through `Database::checkpoint` — a real
4001            // durability barrier, matching PG.
4002            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
4003                self.advance();
4004                self.consume_until_statement_boundary();
4005                Ok(Statement::Empty)
4006            }
4007            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
4008                self.advance();
4009                self.parse_delete_after_keyword()
4010            }
4011            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
4012            // ALTER is not a reserved keyword in the lexer — handled
4013            // as a bare ident here.
4014            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
4015                self.advance();
4016                self.parse_alter_after_keyword()
4017            }
4018            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
4019            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
4020            // additions needed.
4021            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
4022                self.advance();
4023                self.parse_wait_after_keyword()
4024            }
4025            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
4026            // Bare ANALYZE → analyse every user table; ANALYZE
4027            // <name> → re-stats one. The argument is an optional
4028            // ident (or quoted ident); anything else is a parse
4029            // error.
4030            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
4031            // `WHERE` filter (carved out per V6_7_DESIGN.md
4032            // STABILITY). Lex order: identifier "compact" → "cold"
4033            // → "segments". Anything else after `COMPACT` is a
4034            // parse error.
4035            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
4036                self.advance();
4037                let next = self.peek().clone();
4038                let cold = match next {
4039                    Token::Ident(s) | Token::QuotedIdent(s) => s,
4040                    _ => {
4041                        return Err(
4042                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
4043                        );
4044                    }
4045                };
4046                if !cold.eq_ignore_ascii_case("cold") {
4047                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
4048                }
4049                self.advance();
4050                let next = self.peek().clone();
4051                let segments = match next {
4052                    Token::Ident(s) | Token::QuotedIdent(s) => s,
4053                    _ => {
4054                        return Err(self.err(format!(
4055                            "expected SEGMENTS after COMPACT COLD, got {:?}",
4056                            self.peek()
4057                        )));
4058                    }
4059                };
4060                if !segments.eq_ignore_ascii_case("segments") {
4061                    return Err(self.err(format!(
4062                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
4063                    )));
4064                }
4065                self.advance();
4066                Ok(Statement::CompactColdSegments)
4067            }
4068            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
4069            // Parsed as a case-insensitive identifier since MERGE
4070            // isn't a reserved lexer keyword (collides with the
4071            // mysqldump `ALGORITHM = MERGE` view clause if it
4072            // were); the inner parser drives the rest of the
4073            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
4074            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
4075                self.advance();
4076                self.parse_merge_after_keyword()
4077            }
4078            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
4079                self.advance();
4080                // v7.39.9 — MySQL spells it `ANALYZE TABLE t`. The
4081                // keyword is noise to the parse; what differs is the
4082                // ANSWER, which MySQL returns as a result set — see the
4083                // executor.
4084                let mysql_table_kw = matches!(self.peek(), Token::Table);
4085                if mysql_table_kw {
4086                    self.advance();
4087                }
4088                let target = match self.peek() {
4089                    Token::Eof | Token::Semicolon => None,
4090                    Token::Ident(_) | Token::QuotedIdent(_) => {
4091                        Some(self.expect_ident_like()?)
4092                    }
4093                    other => {
4094                        return Err(self.err(format!(
4095                            "expected table name or end of statement after ANALYZE, got {other:?}"
4096                        )));
4097                    }
4098                };
4099                // v7.39 (round 776, F31 J7) — the per-column form
4100                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
4101                // here while the VACUUM arm already consumed it; SPG
4102                // analyzes whole tables, so the list parses and is
4103                // accepted like the VACUUM path's.
4104                if target.is_some() && matches!(self.peek(), Token::LParen) {
4105                    self.advance();
4106                    loop {
4107                        let _ = self.expect_ident_like()?;
4108                        match self.peek() {
4109                            Token::Comma => {
4110                                self.advance();
4111                            }
4112                            Token::RParen => {
4113                                self.advance();
4114                                break;
4115                            }
4116                            other => {
4117                                return Err(self.err(format!(
4118                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
4119                                )));
4120                            }
4121                        }
4122                    }
4123                }
4124                Ok(Statement::Analyze(target))
4125            }
4126            // v7.12.1 — `SET <name> [TO|=] <value>`. The
4127            // `default_text_search_config` parameter is consumed
4128            // by the FTS function dispatcher; other parameter
4129            // names are recorded but treated as a no-op so PG
4130            // dump output loads.
4131            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
4132                self.advance();
4133                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
4134                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
4135                // …` which the SessionVar path handles). `LOCAL` is the only
4136                // one that changes semantics — it scopes the change to the
4137                // current transaction — so capture it; SESSION / GLOBAL are
4138                // accepted and treated as the default session scope.
4139                let mut set_local = false;
4140                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
4141                    let q = s.to_ascii_lowercase();
4142                    if q == "local" || q == "session" || q == "global" {
4143                        set_local = q == "local";
4144                        self.advance();
4145                    }
4146                }
4147                // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
4148                // { DEFAULT | <role> }`. pg_dump's ACL section switches
4149                // to the object owner with it. SPG maps it onto the
4150                // session-role machinery (recorded delta RD-10: PG moves
4151                // session_user too; SPG moves the effective role).
4152                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4153                    if s.eq_ignore_ascii_case("authorization"))
4154                {
4155                    self.advance(); // AUTHORIZATION
4156                    let role = match self.peek().clone() {
4157                        Token::Default => {
4158                            self.advance();
4159                            None
4160                        }
4161                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4162                            self.advance();
4163                            Some(s)
4164                        }
4165                        _ => None,
4166                    };
4167                    return Ok(Statement::SetRole(role));
4168                }
4169                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
4170                // <collation>]` — change the connection client
4171                // charset. SPG stores UTF-8 always and orders
4172                // bytewise; accept as a no-op.
4173                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
4174                {
4175                    self.advance();
4176                    // v7.39 — this used to parse the clause and throw it
4177                    // away ("SPG stores UTF-8 always and orders
4178                    // bytewise; accept as a no-op"). That sentence
4179                    // stopped being true when collations arrived, and
4180                    // once `collation_connection` began driving literal
4181                    // comparison, dropping the COLLATE clause became a
4182                    // silently wrong answer: `SET NAMES utf8mb4 COLLATE
4183                    // utf8mb4_general_ci` reported back
4184                    // `utf8mb4_0900_ai_ci` and compared as NO PAD.
4185                    //
4186                    // The charset name is emitted as `names` and the
4187                    // ENGINE expands it, because which collation a
4188                    // charset defaults to is MySQL semantics and belongs
4189                    // beside the rest of them, not in the parser.
4190                    let mut pairs = alloc::vec::Vec::new();
4191                    if matches!(
4192                        self.peek(),
4193                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4194                    ) {
4195                        let charset = match self.advance() {
4196                            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4197                            _ => unreachable!("peeked an ident-or-string"),
4198                        };
4199                        pairs.push((String::from("names"), crate::ast::SetValue::Ident(charset)));
4200                    }
4201                    // Optional `COLLATE <name>` — emitted AFTER `names`
4202                    // so it overrides the charset's default, which is
4203                    // what MySQL does.
4204                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4205                    {
4206                        self.advance();
4207                        if matches!(
4208                            self.peek(),
4209                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4210                        ) {
4211                            let coll = match self.advance() {
4212                                Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4213                                _ => unreachable!("peeked an ident-or-string"),
4214                            };
4215                            pairs.push((
4216                                String::from("collation_connection"),
4217                                crate::ast::SetValue::Ident(coll),
4218                            ));
4219                        }
4220                    }
4221                    if pairs.is_empty() {
4222                        return Ok(Statement::Empty);
4223                    }
4224                    return Ok(Statement::SetParameterList(pairs));
4225                }
4226                // v7.37.17 (17.6 sibling) — PG `SET ROLE
4227                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4228                // uses this to switch to the object owner before
4229                // recreating tables. SPG has no role system so this
4230                // is a no-op.
4231                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4232                {
4233                    self.advance(); // ROLE
4234                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4235                    // reset to the login identity; a name / string sets the
4236                    // effective role that drives current_user + RLS.
4237                    let role = match self.peek().clone() {
4238                        Token::Default => {
4239                            self.advance();
4240                            None
4241                        }
4242                        Token::Ident(s) | Token::QuotedIdent(s)
4243                            if s.eq_ignore_ascii_case("none") =>
4244                        {
4245                            self.advance();
4246                            None
4247                        }
4248                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4249                            self.advance();
4250                            Some(s)
4251                        }
4252                        _ => None,
4253                    };
4254                    return Ok(Statement::SetRole(role));
4255                }
4256                // v7.37.17 (17.6 sibling) — PG `SET SESSION
4257                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4258                // ISO SQL surface). pg_dump prepends this to fix
4259                // the isolation level for the restore session. SPG
4260                // defaults to READ COMMITTED and doesn't yet honor
4261                // session-set isolation across statements — accept
4262                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4263                // per-tx form is handled elsewhere.
4264                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4265                {
4266                    self.advance(); // CHARACTERISTICS
4267                    if matches!(self.peek(), Token::As) {
4268                        self.advance();
4269                    }
4270                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
4271                        self.advance();
4272                    }
4273                    // v7.39 — no longer a no-op. The note above said SPG
4274                    // "doesn't yet honor session-set isolation across
4275                    // statements"; it does now, through
4276                    // `default_transaction_isolation`, and measured on
4277                    // PG 18.6 this statement is exactly a way to set it:
4278                    //
4279                    //   SET SESSION CHARACTERISTICS AS TRANSACTION
4280                    //       ISOLATION LEVEL REPEATABLE READ;
4281                    //   current_setting('default_transaction_isolation')
4282                    //       -> repeatable read
4283                    //
4284                    // pg_dump prepends this to fix the level for a
4285                    // restore session, so accepting it and doing nothing
4286                    // meant the restore ran at a level nobody chose.
4287                    //
4288                    // v7.39 wired READ ONLY here for the same reason,
4289                    // once something enforced it.
4290                    //
4291                    // v7.40.12 — and DEFERRABLE, once something honoured
4292                    // it. This was the LAST of the three still being
4293                    // consumed and dropped: measured on PG 18.6,
4294                    // `SET SESSION CHARACTERISTICS AS TRANSACTION
4295                    // DEFERRABLE; SHOW default_transaction_deferrable`
4296                    // answers `on`, and SPG answered `off` while
4297                    // answering the READ ONLY and ISOLATION LEVEL forms
4298                    // of the same statement correctly.
4299                    let modes = self.parse_isolation_level_clauses()?;
4300                    self.consume_until_statement_boundary();
4301                    let mut pairs: alloc::vec::Vec<(
4302                        alloc::string::String,
4303                        crate::ast::SetValue,
4304                    )> = alloc::vec::Vec::new();
4305                    if let Some(level) = modes.isolation {
4306                        pairs.push((
4307                            alloc::string::String::from("default_transaction_isolation"),
4308                            crate::ast::SetValue::String(alloc::string::String::from(
4309                                level.as_pg_str(),
4310                            )),
4311                        ));
4312                    }
4313                    if let Some(ro) = modes.read_only {
4314                        pairs.push((
4315                            alloc::string::String::from("default_transaction_read_only"),
4316                            crate::ast::SetValue::Ident(alloc::string::String::from(if ro {
4317                                "on"
4318                            } else {
4319                                "off"
4320                            })),
4321                        ));
4322                    }
4323                    if let Some(d) = modes.deferrable {
4324                        pairs.push((
4325                            alloc::string::String::from("default_transaction_deferrable"),
4326                            crate::ast::SetValue::Ident(alloc::string::String::from(if d {
4327                                "on"
4328                            } else {
4329                                "off"
4330                            })),
4331                        ));
4332                    }
4333                    return Ok(if pairs.is_empty() {
4334                        Statement::Empty
4335                    } else {
4336                        Statement::SetParameterList(pairs)
4337                    });
4338                }
4339                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4340                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4341                // pg_dump emits this to control the deferrability of
4342                // FK / UNIQUE constraints across a bulk restore. SPG
4343                // has no deferrable-constraint machinery today; the
4344                // FK checker is strict-immediate. Accept-and-no-op
4345                // for pg_dump round-trip compatibility.
4346                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4347                {
4348                    self.advance(); // CONSTRAINTS
4349                    // v7.39 (round 288) — no longer a no-op: the trailing
4350                    // DEFERRED / IMMEDIATE sets the transaction's timing.
4351                    // v7.39 (round 308, V29) — and the names are kept.
4352                    // They used to be skipped over on the way to the
4353                    // DEFERRED keyword, so a named form silently behaved
4354                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4355                    // every deferrable constraint in the transaction.
4356                    let mut names: alloc::vec::Vec<alloc::string::String> =
4357                        alloc::vec::Vec::new();
4358                    if matches!(self.peek(), Token::All) {
4359                        self.advance();
4360                    } else {
4361                        loop {
4362                            let mut n = self.expect_ident_like()?;
4363                            // A schema-qualified name (`public.fk_a`)
4364                            // identifies the same constraint; PG resolves
4365                            // it by the trailing segment.
4366                            while matches!(self.peek(), Token::Dot) {
4367                                self.advance();
4368                                n = self.expect_ident_like()?;
4369                            }
4370                            names.push(n);
4371                            if matches!(self.peek(), Token::Comma) {
4372                                self.advance();
4373                            } else {
4374                                break;
4375                            }
4376                        }
4377                    }
4378                    let deferred = match self.peek() {
4379                        Token::Ident(s) | Token::QuotedIdent(s)
4380                            if s.eq_ignore_ascii_case("deferred") =>
4381                        {
4382                            true
4383                        }
4384                        Token::Ident(s) | Token::QuotedIdent(s)
4385                            if s.eq_ignore_ascii_case("immediate") =>
4386                        {
4387                            false
4388                        }
4389                        other => {
4390                            return Err(self.err(alloc::format!(
4391                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4392                            )));
4393                        }
4394                    };
4395                    self.advance();
4396                    return Ok(Statement::SetConstraints { names, deferred });
4397                }
4398                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4399                // { DEFAULT | '<role>' | <ident> }` (mailrs
4400                // round-10 A.1). pg_dump preamble emits the
4401                // `DEFAULT` form to reset session authorization.
4402                //
4403                // v7.39 (round 697) — this said "SPG has no role system so
4404                // this is a strict no-op". SPG has had one since round 58;
4405                // the comment outlived it, and with it the reason a name
4406                // that is not a role was accepted here. It still switches
4407                // no authorization — what it does now is refuse a role
4408                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4409                // AUTHORIZATION` (handled by the RESET parser
4410                // elsewhere). Reference:
4411                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4412                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4413                {
4414                    self.advance(); // AUTHORIZATION
4415                    match self.peek().clone() {
4416                        Token::Default => {
4417                            self.advance();
4418                        }
4419                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4420                            self.advance();
4421                            return Ok(Statement::ValidateOnly {
4422                                kind: crate::ast::ValidateOnlyKind::SessionAuthorization,
4423                                names: alloc::vec![r],
4424                            });
4425                        }
4426                        other => {
4427                            return Err(self.err(alloc::format!(
4428                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4429                            )));
4430                        }
4431                    }
4432                    return Ok(Statement::Empty);
4433                }
4434                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4435                // ISOLATION LEVEL { READ COMMITTED | READ
4436                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4437                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4438                // PG-standard surface. v7.37.8 accepts the syntax
4439                // and tracks the selected level on
4440                // `Engine::current_isolation_level()`; the actual
4441                // MVCC / SSI semantics implementation lands in
4442                // the 轴 4 isolation framework (separate train).
4443                // PG itself maps READ UNCOMMITTED to READ COMMITTED
4444                // internally; SPG behaves the same (effectively
4445                // READ COMMITTED at every level today).
4446                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4447                {
4448                    self.advance(); // TRANSACTION
4449                    let modes = self.parse_isolation_level_clauses()?;
4450                    return Ok(Statement::SetTransaction { modes });
4451                }
4452                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4453                // alias — same accept-as-no-op as SET NAMES.
4454                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4455                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4456                {
4457                    self.advance(); // CHARACTER
4458                    self.advance(); // SET
4459                    if matches!(
4460                        self.peek(),
4461                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4462                    ) {
4463                        self.advance();
4464                    }
4465                    return Ok(Statement::Empty);
4466                }
4467                // v7.39 (GUC) — PG spells the timezone GUC as two
4468                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4469                // where <value> is a string/ident or the LOCAL /
4470                // DEFAULT keyword (both mean "back to the default").
4471                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4472                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4473                {
4474                    self.advance(); // TIME
4475                    self.advance(); // ZONE
4476                    let value = match self.peek().clone() {
4477                        Token::Ident(s)
4478                            if s.eq_ignore_ascii_case("local")
4479                                || s.eq_ignore_ascii_case("default") =>
4480                        {
4481                            self.advance();
4482                            crate::ast::SetValue::Default
4483                        }
4484                        Token::Default => {
4485                            self.advance();
4486                            crate::ast::SetValue::Default
4487                        }
4488                        _ => self.parse_set_value()?,
4489                    };
4490                    return Ok(Statement::SetParameter {
4491                        name: "timezone".into(),
4492                        value,
4493                        local: set_local,
4494                    });
4495                }
4496                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4497                // MySQL USER-variable assignment: its own per-session
4498                // namespace, an arbitrary expression on the right, and `:=`
4499                // as a second spelling of `=`. It used to fall into the
4500                // session-PARAMETER list below, whose values are literals and
4501                // whose store nothing reads back under a `@` name — so the
4502                // assignment reported success and vanished.
4503                //
4504                // A `@@`-prefixed LHS is a real engine setting and keeps the
4505                // old path.
4506                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4507                    return self.parse_set_user_vars();
4508                }
4509                // v7.14.0 — multi-assignment form
4510                // `SET a = 1, b = 2, …`. Single-assignment is the
4511                // 1-element case. Each LHS may be a regular ident
4512                // or a SessionVar (`@VAR` / `@@VAR`).
4513                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4514                loop {
4515                    let lhs = match self.peek().clone() {
4516                        Token::SessionVar(s) => {
4517                            self.advance();
4518                            s
4519                        }
4520                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4521                        other => {
4522                            return Err(self.err(format!(
4523                                "expected parameter name after SET, got {other:?}"
4524                            )));
4525                        }
4526                    };
4527                    // Accept either `=` or the bare `TO` keyword.
4528                    match self.peek() {
4529                        Token::Eq => {
4530                            self.advance();
4531                        }
4532                        Token::To => {
4533                            self.advance();
4534                        }
4535                        other => {
4536                            return Err(self.err(format!(
4537                                "expected `=` or TO after SET {lhs}, got {other:?}"
4538                            )));
4539                        }
4540                    }
4541                    let mut value = self.parse_set_value()?;
4542                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4543                    // `, name TO` continues a MySQL-style multi-assign,
4544                    // anything else is a PG list VALUE
4545                    // (`SET search_path = myschema, public`) folded into
4546                    // one comma-joined string.
4547                    while matches!(self.peek(), Token::Comma) {
4548                        let is_assign = matches!(
4549                            self.tokens.get(self.pos + 1),
4550                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4551                        ) && matches!(
4552                            self.tokens.get(self.pos + 2),
4553                            Some(Token::Eq | Token::To)
4554                        );
4555                        if is_assign {
4556                            break;
4557                        }
4558                        self.advance(); // comma
4559                        let next = self.parse_set_value()?;
4560                        let joined = alloc::format!(
4561                            "{}, {}",
4562                            set_value_text(&value),
4563                            set_value_text(&next)
4564                        );
4565                        value = crate::ast::SetValue::String(joined);
4566                    }
4567                    pairs.push((lhs, value));
4568                    if matches!(self.peek(), Token::Comma) {
4569                        self.advance();
4570                        continue;
4571                    }
4572                    break;
4573                }
4574                if pairs.len() == 1 {
4575                    let (name, value) = pairs.into_iter().next().unwrap();
4576                    Ok(Statement::SetParameter {
4577                        name,
4578                        value,
4579                        local: set_local,
4580                    })
4581                } else {
4582                    Ok(Statement::SetParameterList(pairs))
4583                }
4584            }
4585            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4586            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4587                self.advance();
4588                match self.peek().clone() {
4589                    Token::All => {
4590                        self.advance();
4591                        Ok(Statement::ResetParameter(None))
4592                    }
4593                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4594                        self.advance();
4595                        Ok(Statement::ResetParameter(None))
4596                    }
4597                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4598                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4599                        self.advance();
4600                        Ok(Statement::SetRole(None))
4601                    }
4602                    // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4603                    // (pg_dump's return from the owner switch).
4604                    Token::Ident(s) | Token::QuotedIdent(s)
4605                        if s.eq_ignore_ascii_case("session")
4606                            && matches!(
4607                                self.tokens.get(self.pos + 1),
4608                                Some(Token::Ident(a) | Token::QuotedIdent(a))
4609                                    if a.eq_ignore_ascii_case("authorization")
4610                            ) =>
4611                    {
4612                        self.advance(); // SESSION
4613                        self.advance(); // AUTHORIZATION
4614                        Ok(Statement::SetRole(None))
4615                    }
4616                    _ => {
4617                        let name = self.parse_set_param_name()?;
4618                        Ok(Statement::ResetParameter(Some(name)))
4619                    }
4620                }
4621            }
4622            // v7.39 (round 218) — server-side cursors.
4623            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4624            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4625            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4626            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4627                self.advance();
4628                match self.peek().clone() {
4629                    Token::All => {
4630                        self.advance();
4631                        Ok(Statement::CloseCursor { name: None })
4632                    }
4633                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4634                        self.advance();
4635                        Ok(Statement::CloseCursor { name: None })
4636                    }
4637                    Token::Ident(n) | Token::QuotedIdent(n) => {
4638                        self.advance();
4639                        Ok(Statement::CloseCursor { name: Some(n) })
4640                    }
4641                    other => Err(self.err(format!(
4642                        "expected cursor name or ALL after CLOSE, got {other:?}"
4643                    ))),
4644                }
4645            }
4646            other => Err(self.err(format!(
4647                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4648                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4649            ))),
4650        }
4651    }
4652
4653    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4654    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4655    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4656    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4657    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4658        self.advance(); // DECLARE
4659        let name = match self.advance() {
4660            Token::Ident(n) | Token::QuotedIdent(n) => n,
4661            other => {
4662                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4663            }
4664        };
4665        let mut scroll: Option<bool> = None;
4666        loop {
4667            match self.peek() {
4668                Token::Ident(s)
4669                    if s.eq_ignore_ascii_case("binary")
4670                        || s.eq_ignore_ascii_case("insensitive")
4671                        || s.eq_ignore_ascii_case("asensitive") =>
4672                {
4673                    self.advance();
4674                }
4675                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4676                    self.advance();
4677                    scroll = Some(true);
4678                }
4679                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4680                {
4681                    self.advance(); // NO
4682                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4683                        return Err(self.err(format!(
4684                            "expected SCROLL after NO in DECLARE, got {:?}",
4685                            self.peek()
4686                        )));
4687                    }
4688                    self.advance();
4689                    scroll = Some(false);
4690                }
4691                _ => break,
4692            }
4693        }
4694        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4695            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4696        }
4697        self.advance();
4698        let mut hold = false;
4699        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4700            self.advance();
4701            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4702                return Err(self.err(format!(
4703                    "expected HOLD after WITH in DECLARE, got {:?}",
4704                    self.peek()
4705                )));
4706            }
4707            self.advance();
4708            hold = true;
4709        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4710            self.advance();
4711            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4712                return Err(self.err(format!(
4713                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4714                    self.peek()
4715                )));
4716            }
4717            self.advance();
4718        }
4719        if !matches!(self.peek(), Token::For) {
4720            return Err(self.err(format!(
4721                "expected FOR before the cursor query, got {:?}",
4722                self.peek()
4723            )));
4724        }
4725        self.advance();
4726        let query = self.parse_one_statement()?;
4727        Ok(Statement::DeclareCursor {
4728            name,
4729            scroll,
4730            hold,
4731            query: alloc::boxed::Box::new(query),
4732        })
4733    }
4734
4735    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4736    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4737    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4738    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4739        use crate::ast::CursorDirection as D;
4740        self.advance(); // FETCH / MOVE
4741        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4742            let neg = if matches!(this.peek(), Token::Minus) {
4743                this.advance();
4744                true
4745            } else {
4746                false
4747            };
4748            match this.advance() {
4749                Token::Integer(v) => Ok(if neg { -v } else { v }),
4750                other => Err(this.err(format!("expected count, got {other:?}"))),
4751            }
4752        };
4753        let direction = match self.peek().clone() {
4754            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4755                self.advance();
4756                D::Next
4757            }
4758            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4759                self.advance();
4760                D::Prior
4761            }
4762            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4763                self.advance();
4764                D::First
4765            }
4766            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4767                self.advance();
4768                D::Last
4769            }
4770            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4771                self.advance();
4772                D::Absolute(signed_count(self)?)
4773            }
4774            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4775                self.advance();
4776                D::Relative(signed_count(self)?)
4777            }
4778            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4779                self.advance();
4780                match self.peek().clone() {
4781                    Token::All => {
4782                        self.advance();
4783                        D::All
4784                    }
4785                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4786                        self.advance();
4787                        D::All
4788                    }
4789                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4790                    _ => D::Next, // bare FORWARD = FORWARD 1
4791                }
4792            }
4793            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4794                self.advance();
4795                match self.peek().clone() {
4796                    Token::All => {
4797                        self.advance();
4798                        D::BackwardAll
4799                    }
4800                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4801                        self.advance();
4802                        D::BackwardAll
4803                    }
4804                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4805                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4806                }
4807            }
4808            Token::All => {
4809                self.advance();
4810                D::All
4811            }
4812            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4813                self.advance();
4814                D::All
4815            }
4816            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4817            // Bare `FETCH <name>` — direction defaults to NEXT.
4818            _ => D::Next,
4819        };
4820        // Optional FROM / IN.
4821        if matches!(self.peek(), Token::From)
4822            || matches!(self.peek(), Token::In)
4823            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4824        {
4825            self.advance();
4826        }
4827        let name = match self.advance() {
4828            Token::Ident(n) | Token::QuotedIdent(n) => n,
4829            other => {
4830                return Err(self.err(format!("expected cursor name, got {other:?}")));
4831            }
4832        };
4833        Ok(if is_move {
4834            Statement::MoveCursor { name, direction }
4835        } else {
4836            Statement::FetchCursor { name, direction }
4837        })
4838    }
4839
4840    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4841    /// [(kind, …)] ON <col>, … FROM <table>`.
4842    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4843        self.advance(); // STATISTICS
4844        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4845        let mut if_not_exists = false;
4846        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4847            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4848        {
4849            self.advance();
4850            self.advance();
4851            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4852                self.advance();
4853                if_not_exists = true;
4854            }
4855        }
4856        let name = self.expect_ident_like()?;
4857        let mut kinds = Vec::new();
4858        if matches!(self.peek(), Token::LParen) {
4859            self.advance();
4860            loop {
4861                let k = self.expect_ident_like()?;
4862                // PG stores the single letters; accept the spelled-out
4863                // names the SQL uses and record what PG records.
4864                kinds.push(match k.to_ascii_lowercase().as_str() {
4865                    "ndistinct" => String::from("d"),
4866                    "dependencies" => String::from("f"),
4867                    "mcv" => String::from("m"),
4868                    other => {
4869                        return Err(
4870                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4871                        );
4872                    }
4873                });
4874                match self.advance() {
4875                    Token::Comma => {}
4876                    Token::RParen => break,
4877                    other => {
4878                        return Err(self.err(alloc::format!(
4879                            "expected ',' or ')' in statistics kind list, got {other:?}"
4880                        )));
4881                    }
4882                }
4883            }
4884        }
4885        if !matches!(self.peek(), Token::On) {
4886            return Err(self.err(alloc::format!(
4887                "expected ON in CREATE STATISTICS, got {:?}",
4888                self.peek()
4889            )));
4890        }
4891        self.advance();
4892        let mut columns = Vec::new();
4893        loop {
4894            columns.push(self.expect_ident_like()?);
4895            if matches!(self.peek(), Token::Comma) {
4896                self.advance();
4897            } else {
4898                break;
4899            }
4900        }
4901        if !matches!(self.peek(), Token::From) {
4902            return Err(self.err(alloc::format!(
4903                "expected FROM in CREATE STATISTICS, got {:?}",
4904                self.peek()
4905            )));
4906        }
4907        self.advance();
4908        let table = self.expect_ident_like()?;
4909        Ok(Statement::CreateStatistics {
4910            name,
4911            if_not_exists,
4912            kinds,
4913            columns,
4914            table,
4915        })
4916    }
4917
4918    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4919    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4920    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4921    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4922    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4923    /// forward call.
4924    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4925        self.advance(); // TABLE
4926        let if_exists = self.consume_if_exists();
4927        let mut names: Vec<String> = Vec::new();
4928        loop {
4929            names.push(self.expect_ident_like()?);
4930            if matches!(self.peek(), Token::Comma) {
4931                self.advance();
4932                continue;
4933            }
4934            break;
4935        }
4936        if matches!(
4937            self.peek(),
4938            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4939                || s.eq_ignore_ascii_case("restrict")
4940        ) {
4941            self.advance();
4942        }
4943        Ok(Statement::DropTable { names, if_exists })
4944    }
4945
4946    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4947        self.advance(); // STATISTICS
4948        let mut if_exists = false;
4949        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4950            && matches!(self.tokens.get(self.pos + 1),
4951                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4952        {
4953            self.advance();
4954            self.advance();
4955            if_exists = true;
4956        }
4957        let name = self.expect_ident_like()?;
4958        Ok(Statement::DropStatistics { name, if_exists })
4959    }
4960
4961    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4962        debug_assert!(matches!(self.peek(), Token::Create));
4963        self.advance();
4964        match self.peek() {
4965            Token::Table => self.parse_create_table_stmt_after_create(),
4966            Token::Index => self.parse_create_index_stmt_after_create(false),
4967            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4968            // object now. It used to be consumed by the CREATE-noise
4969            // arm, so a pg_dump that declares extended statistics
4970            // restored silently without them and reflection showed
4971            // nothing.
4972            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4973                self.parse_create_statistics_after_create()
4974            }
4975            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4976            // The `UNIQUE` modifier turns a partial index into a
4977            // partial-uniqueness invariant (only rows matching the
4978            // WHERE predicate are checked for duplicates). mailrs
4979            // K1 (3 hits: email_templates default, calendar_events
4980            // master, calendar_events instance).
4981            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4982                self.advance();
4983                if !matches!(self.peek(), Token::Index) {
4984                    return Err(self.err(alloc::format!(
4985                        "expected INDEX after CREATE UNIQUE, got {:?}",
4986                        self.peek()
4987                    )));
4988                }
4989                self.parse_create_index_stmt_after_create(true)
4990            }
4991            Token::Publication => {
4992                self.advance();
4993                self.parse_create_publication_after_keyword()
4994            }
4995            Token::Subscription => {
4996                self.advance();
4997                self.parse_create_subscription_after_keyword()
4998            }
4999            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
5000            // USER isn't a reserved keyword — we look for the bare
5001            // identifier so the lexer doesn't have to grow a token.
5002            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
5003                self.advance();
5004                self.parse_create_user_after_keyword(true)
5005            }
5006            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
5007            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
5008            // the default of the LOGIN attribute.
5009            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
5010                self.advance();
5011                self.parse_create_user_after_keyword(false)
5012            }
5013            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
5014            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
5015                self.advance();
5016                self.parse_create_policy_after_keyword()
5017            }
5018            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
5019            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
5020            // no-op. mailrs follow-up F3.
5021            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
5022                self.advance();
5023                self.parse_create_extension_after_keyword()
5024            }
5025            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
5026            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
5027            // optional; absorb it here and forward to the
5028            // per-kind parsers with the flag. OR is a reserved
5029            // keyword token.
5030            Token::Or => {
5031                self.advance();
5032                let next = self.peek();
5033                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
5034                    return Err(self.err(alloc::format!(
5035                        "expected REPLACE after CREATE OR, got {next:?}"
5036                    )));
5037                };
5038                if !s2.eq_ignore_ascii_case("replace") {
5039                    return Err(self.err(alloc::format!(
5040                        "expected REPLACE after CREATE OR, got {s2:?}"
5041                    )));
5042                }
5043                self.advance();
5044                self.parse_create_function_or_trigger_after_or_replace(true)
5045            }
5046            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
5047                self.advance();
5048                self.parse_create_function_after_keyword(false)
5049            }
5050            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
5051                self.advance();
5052                self.parse_create_trigger_after_keyword(false)
5053            }
5054            // v7.39 (round 139) — CREATE RULE …
5055            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
5056                self.advance();
5057                self.parse_create_rule_after_keyword(false)
5058            }
5059            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
5060            // trigger is a row-level AFTER trigger that additionally carries
5061            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
5062            // path already tolerates and skips those clauses, so consuming the
5063            // CONSTRAINT keyword and reusing it makes the statement parse and the
5064            // trigger fire. (The deferral timing itself is not yet honoured —
5065            // SPG fires it as a plain AFTER trigger, which is correct behaviour
5066            // for every non-deferred use.)
5067            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5068                self.advance();
5069                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
5070                    if t.eq_ignore_ascii_case("trigger"))
5071                {
5072                    return Err(self.err(alloc::format!(
5073                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
5074                        self.peek()
5075                    )));
5076                }
5077                self.advance();
5078                self.parse_create_trigger_after_keyword(false)
5079            }
5080            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
5081            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
5082                self.advance();
5083                self.parse_create_sequence_after_keyword(false)
5084            }
5085            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
5086            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
5087                self.advance();
5088                self.parse_create_view_after_keyword(false, false, false)
5089            }
5090            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
5091            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
5092            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
5093            // appear (in any order) between `CREATE` and `VIEW` in
5094            // every mysqldump-emitted view. Pre-2.6 the parser
5095            // rejected the prefix and the customer's whole view
5096            // backup failed on the first view. The hints are pure
5097            // planner / permission metadata; SPG's view-rewrite
5098            // path is semantically equivalent for all three
5099            // algorithms in v7.17 (TEMPTABLE differs only in
5100            // perf for huge views — out of v7.17 scope), and
5101            // DEFINER / SQL SECURITY are pure single-user
5102            // permissioning that SPG ignores by design.
5103            Token::Ident(s) | Token::QuotedIdent(s)
5104                if s.eq_ignore_ascii_case("algorithm")
5105                    || s.eq_ignore_ascii_case("definer")
5106                    || s.eq_ignore_ascii_case("sql") =>
5107            {
5108                self.consume_mysql_view_prefix()?;
5109                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
5110                // (in any order, in any combination), the next
5111                // keyword must be VIEW. mysqldump never emits these
5112                // prefixes on non-view statements.
5113                let next = self.peek().clone();
5114                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
5115                    if s2.eq_ignore_ascii_case("view"))
5116                {
5117                    self.advance();
5118                    self.parse_create_view_after_keyword(false, false, false)
5119                } else {
5120                    Err(self.err(alloc::format!(
5121                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
5122                    )))
5123                }
5124            }
5125            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
5126            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
5127                self.advance();
5128                self.parse_create_type_after_keyword()
5129            }
5130            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
5131            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
5132            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
5133                self.advance();
5134                self.parse_create_domain_after_keyword()
5135            }
5136            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
5137            // name [AUTHORIZATION user]. Real catalog registry
5138            // (was silent-no-op'd pre-v7.17).
5139            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
5140                self.advance();
5141                let if_not_exists = self.parse_if_not_exists();
5142                let name = self.expect_ident_like()?;
5143                // Optional `AUTHORIZATION <user>` trailer — accepted,
5144                // ignored (single-user catalog).
5145                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
5146                    if s.eq_ignore_ascii_case("authorization"))
5147                {
5148                    self.advance();
5149                    let _ = self.expect_ident_like()?;
5150                }
5151                Ok(Statement::CreateSchema { name, if_not_exists })
5152            }
5153            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
5154            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
5155                self.advance();
5156                let next = self.peek().clone();
5157                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5158                {
5159                    self.advance();
5160                    self.parse_create_materialized_view_after_keyword()
5161                } else {
5162                    Err(self.err(alloc::format!(
5163                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
5164                    )))
5165                }
5166            }
5167            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
5168            // no-op below), an UNLOGGED table is a real, fully-usable table in
5169            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
5170            // durability optimisation is a follow-up), so a dump / app that
5171            // declares UNLOGGED tables works instead of failing to parse.
5172            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
5173                self.advance(); // UNLOGGED
5174                if matches!(self.peek(), Token::Table) {
5175                    self.parse_create_table_stmt_after_create()
5176                } else {
5177                    Err(self.err(format!(
5178                        "expected TABLE after CREATE UNLOGGED, got {:?}",
5179                        self.peek()
5180                    )))
5181                }
5182            }
5183            Token::Ident(s) | Token::QuotedIdent(s)
5184                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
5185            {
5186                self.advance();
5187                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
5188                let next = self.peek().clone();
5189                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
5190                {
5191                    self.advance();
5192                    self.parse_create_sequence_after_keyword(true)
5193                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5194                {
5195                    self.advance();
5196                    self.parse_create_view_after_keyword(false, false, true)
5197                } else {
5198                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
5199                    // consumed and answered OK while creating nothing, so
5200                    // every statement that touched the table afterwards failed
5201                    // with "table not found" — the DDL itself lied. It is a
5202                    // real CREATE TABLE now, marked temporary so the executor
5203                    // puts it in the session's own namespace. An optional
5204                    // TABLE keyword may or may not be present (`CREATE TEMP t`
5205                    // is not legal, but the keyword is consumed by the
5206                    // CREATE TABLE parser itself).
5207                    let stmt = self.parse_create_table_stmt_after_create()?;
5208                    match stmt {
5209                        Statement::CreateTable(mut c) => {
5210                            c.temporary = true;
5211                            Ok(Statement::CreateTable(c))
5212                        }
5213                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
5214                        // CTAS node, which needs the same session namespace.
5215                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
5216                            m.temporary = true;
5217                            Ok(Statement::CreateMaterializedView(m))
5218                        }
5219                        other => Ok(other),
5220                    }
5221                }
5222            }
5223            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
5224            // BEGIN <body> END`. The body may reference `@var`
5225            // session variables, SET statements, internal `;`
5226            // terminators, etc. SPG has no procedure runtime, so
5227            // consume the whole `CREATE PROCEDURE … END` block as
5228            // a no-op so mysqldump scripts that include stored
5229            // routines load through. The matching-END consumer
5230            // tracks BEGIN/END nesting depth to handle nested
5231            // BEGIN blocks correctly.
5232            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
5233                self.consume_mysql_routine_body();
5234                Ok(Statement::Empty)
5235            }
5236            // v7.14.0 — pg_dump / mysqldump emit
5237            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
5238            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
5239            // SPG is single-schema / single-database; these have
5240            // no behavioural effect, so consume + return Empty.
5241            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
5242            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
5243            // moved up to real parser branches. DATABASE / ROLE /
5244            // POLICY / OPERATOR stay no-op forever
5245            // (single-database, hardcoded roles).
5246            Token::Ident(s) | Token::QuotedIdent(s)
5247                if matches!(
5248                    s.to_ascii_lowercase().as_str(),
5249                    "database"
5250                        | "role"
5251                        | "operator"
5252                        | "cast"
5253                        | "aggregate"
5254                        | "language"
5255                        | "collation"
5256                        | "conversion"
5257                        // v7.17.0 Phase 8 (audit N6) — rarely-
5258                        // emitted pg_dump shapes that should
5259                        // load through without a parser error.
5260                        // SPG has no planner statistics catalog,
5261                        // no event-trigger hooks, no foreign-
5262                        // data-wrapper infrastructure; consume
5263                        // + return Empty.
5264                        | "statistics"
5265                        | "event"
5266                        // v7.37.17 (17.6 siblings) — additional CREATE
5267                        // targets pg_dump / operator install scripts
5268                        // may emit that SPG has no matching machinery
5269                        // for. Consume + Empty-return.
5270                        | "text"
5271                        | "tablespace"
5272                        | "access"
5273                        | "large"
5274                ) =>
5275            {
5276                // DATABASE is the one member of this list PG refuses
5277                // inside a transaction block; the rest (ROLE, CAST,
5278                // TABLESPACE, …) it runs there quite happily, so only
5279                // this one is named. Still a no-op otherwise — SPG is
5280                // single-database.
5281                let is_database = s.eq_ignore_ascii_case("database");
5282                // The name is the first token after DATABASE, past an
5283                // `IF NOT EXISTS`.
5284                let name = if is_database {
5285                    self.scan_database_name()
5286                } else {
5287                    None
5288                };
5289                let collation = if is_database {
5290                    self.scan_database_collation_until_boundary()
5291                } else {
5292                    self.consume_until_statement_boundary();
5293                    None
5294                };
5295                if is_database {
5296                    return Ok(Statement::NoOpPreventedInTransaction {
5297                        what: String::from("CREATE DATABASE"),
5298                        collation,
5299                        name,
5300                    });
5301                }
5302                Ok(Statement::Empty)
5303            }
5304            // v7.39 (round 706) — the foreign-data family leaves the silent
5305            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5306            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5307            // FDW machinery), but the ENGINE now warns, so a restore log
5308            // says what will not function instead of reporting success.
5309            Token::Ident(s) | Token::QuotedIdent(s)
5310                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5311            {
5312                self.consume_until_statement_boundary();
5313                Ok(Statement::ValidateOnly {
5314                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5315                    names: Vec::new(),
5316                })
5317            }
5318            other => Err(self.err(format!(
5319                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5320            ))),
5321        }
5322    }
5323
5324    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5325    /// keyword decides whether we parse a function or trigger
5326    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5327    /// PROCEDURE) — those land in later releases.
5328    fn parse_create_function_or_trigger_after_or_replace(
5329        &mut self,
5330        or_replace: bool,
5331    ) -> Result<Statement, ParseError> {
5332        let tok = self.peek();
5333        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5334            return Err(self.err(alloc::format!(
5335                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5336            )));
5337        };
5338        if s.eq_ignore_ascii_case("function") {
5339            self.advance();
5340            self.parse_create_function_after_keyword(or_replace)
5341        } else if s.eq_ignore_ascii_case("trigger") {
5342            self.advance();
5343            self.parse_create_trigger_after_keyword(or_replace)
5344        } else if s.eq_ignore_ascii_case("rule") {
5345            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5346            self.advance();
5347            self.parse_create_rule_after_keyword(or_replace)
5348        } else if s.eq_ignore_ascii_case("view") {
5349            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5350            self.advance();
5351            self.parse_create_view_after_keyword(or_replace, false, false)
5352        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5353            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5354            self.advance();
5355            let nxt = self.peek().clone();
5356            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5357            {
5358                self.advance();
5359                self.parse_create_view_after_keyword(or_replace, false, true)
5360            } else {
5361                Err(self.err(alloc::format!(
5362                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5363                )))
5364            }
5365        } else {
5366            Err(self.err(alloc::format!(
5367                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5368            )))
5369        }
5370    }
5371
5372    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5373    /// SPG doesn't have a registry; pgvector / similar are
5374    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5375    /// the syntax lets dual-target schemas keep the line.
5376    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5377        // Optional `IF NOT EXISTS`.
5378        self.consume_if_not_exists();
5379        let name = self.expect_ident_like()?;
5380        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5381        // CASCADE / FROM '<v>' clauses; we don't model them.
5382        loop {
5383            match self.peek() {
5384                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5385                    self.advance();
5386                    continue;
5387                }
5388                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5389                    self.advance();
5390                    let _ = self.expect_ident_like()?;
5391                    continue;
5392                }
5393                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5394                    self.advance();
5395                    // String or ident literal.
5396                    let _ = self.advance();
5397                    continue;
5398                }
5399                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5400                    self.advance();
5401                    let _ = self.advance();
5402                    continue;
5403                }
5404                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5405                    self.advance();
5406                    continue;
5407                }
5408                _ => break,
5409            }
5410        }
5411        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5412        // nosuch` reported success and `pg_extension` then did not list it,
5413        // which is the accept-and-do-nothing shape F31 exists to find.
5414        Ok(Statement::ValidateOnly {
5415            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5416            names: alloc::vec![name],
5417        })
5418    }
5419
5420    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5421    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5422    /// already been consumed by the caller. Grammar accepted:
5423    ///
5424    ///   name `(` arg-list `)`
5425    ///   `RETURNS` return-type
5426    ///   [ `LANGUAGE` ident ]
5427    ///   `AS` $$ body $$
5428    ///   [ `LANGUAGE` ident ]
5429    ///
5430    /// Either `LANGUAGE` position is allowed; PG accepts both.
5431    fn parse_create_function_after_keyword(
5432        &mut self,
5433        or_replace: bool,
5434    ) -> Result<Statement, ParseError> {
5435        let name = self.expect_ident_like()?;
5436        // Argument list. v7.12.4 commonly sees the empty `()`
5437        // (trigger functions); typed args parse and round-trip
5438        // but the executor only invokes nullary functions.
5439        if !matches!(self.peek(), Token::LParen) {
5440            return Err(self.err(alloc::format!(
5441                "expected '(' after function name {name:?}, got {:?}",
5442                self.peek()
5443            )));
5444        }
5445        self.advance();
5446        let args = self.parse_function_arg_list()?;
5447        // RETURNS clause.
5448        let tok = self.peek();
5449        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5450            return Err(self.err(alloc::format!(
5451                "expected RETURNS after function arg list, got {tok:?}"
5452            )));
5453        };
5454        if !s.eq_ignore_ascii_case("returns") {
5455            return Err(self.err(alloc::format!(
5456                "expected RETURNS after function arg list, got {s:?}"
5457            )));
5458        }
5459        self.advance();
5460        let returns = self.parse_function_return()?;
5461        // Optional LANGUAGE clause (PG also accepts after AS — we'll
5462        // re-check after the body too).
5463        let mut language: Option<String> = self.parse_optional_language()?;
5464        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5465        // either side of the body and in any order, interleaved with
5466        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5467        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5468        // PG's own pg_dump output did not restore.
5469        let mut attrs = FunctionAttrs::default();
5470        loop {
5471            let before = self.pos;
5472            self.parse_function_attrs_into(&mut attrs)?;
5473            if language.is_none() {
5474                language = self.parse_optional_language()?;
5475            }
5476            if self.pos == before {
5477                break;
5478            }
5479        }
5480        // `AS` followed by a $$-quoted body (lexer already
5481        // collapses both `$$…$$` and `$tag$…$tag$` to a single
5482        // Token::String). AS is a reserved keyword (Token::As).
5483        if !matches!(self.peek(), Token::As) {
5484            return Err(self.err(alloc::format!(
5485                "expected AS before function body, got {:?}",
5486                self.peek()
5487            )));
5488        }
5489        self.advance();
5490        let body_text = match self.peek() {
5491            Token::String(s) => {
5492                let body = s.clone();
5493                self.advance();
5494                body
5495            }
5496            other => {
5497                return Err(self.err(alloc::format!(
5498                    "expected $$-quoted function body after AS, got {other:?}"
5499                )));
5500            }
5501        };
5502        // Trailing clauses — PG's other accepted position for both the
5503        // LANGUAGE and the attributes.
5504        loop {
5505            let before = self.pos;
5506            self.parse_function_attrs_into(&mut attrs)?;
5507            if language.is_none() {
5508                language = self.parse_optional_language()?;
5509            }
5510            if self.pos == before {
5511                break;
5512            }
5513        }
5514        let language = language.unwrap_or_else(|| String::from("sql"));
5515        // PL/pgSQL bodies get structure-parsed. Other languages
5516        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5517        // recognise) round-trip as Raw text — the executor errors
5518        // when invoked with a clear unsupported message.
5519        let body = if language.eq_ignore_ascii_case("plpgsql") {
5520            match parse_plpgsql_body(&body_text) {
5521                Ok(block) => FunctionBody::PlPgSql(block),
5522                // Best-effort: if the body parser doesn't yet
5523                // support a construct used inside, fall back to
5524                // raw — keeps `CREATE FUNCTION` itself working
5525                // (catalogue accepts), executor errors on
5526                // invocation only.
5527                Err(_) => FunctionBody::Raw(body_text),
5528            }
5529        } else {
5530            FunctionBody::Raw(body_text)
5531        };
5532        Ok(Statement::CreateFunction(CreateFunctionStatement {
5533            name,
5534            or_replace,
5535            args,
5536            returns,
5537            language,
5538            body,
5539            attrs,
5540        }))
5541    }
5542
5543    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5544    /// attribute clauses into `attrs`, stopping at the first token that
5545    /// is not one. Measured against PG 18.4, which accepts them in any
5546    /// order and on either side of the body.
5547    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5548        loop {
5549            let word = match self.peek() {
5550                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5551                // NOT LEAKPROOF — NOT is a reserved keyword token.
5552                Token::Not
5553                    if matches!(
5554                        self.tokens.get(self.pos + 1),
5555                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5556                    ) =>
5557                {
5558                    self.advance();
5559                    self.advance();
5560                    attrs.leakproof = false;
5561                    continue;
5562                }
5563                _ => return Ok(()),
5564            };
5565            match word.as_str() {
5566                "immutable" => {
5567                    self.advance();
5568                    attrs.volatility = FunctionVolatility::Immutable;
5569                }
5570                "stable" => {
5571                    self.advance();
5572                    attrs.volatility = FunctionVolatility::Stable;
5573                }
5574                "volatile" => {
5575                    self.advance();
5576                    attrs.volatility = FunctionVolatility::Volatile;
5577                }
5578                "strict" => {
5579                    self.advance();
5580                    attrs.strict = true;
5581                }
5582                "leakproof" => {
5583                    self.advance();
5584                    attrs.leakproof = true;
5585                }
5586                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5587                // spelled-out forms of STRICT and its opposite.
5588                "returns" | "called" => {
5589                    let strict = word == "returns";
5590                    let mut probe = self.pos + 1;
5591                    if strict {
5592                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5593                        // is not ours.
5594                        match self.tokens.get(probe) {
5595                            Some(Token::Null) => probe += 1,
5596                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5597                            _ => return Ok(()),
5598                        }
5599                    }
5600                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5601                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5602                    if !ok {
5603                        return Ok(());
5604                    }
5605                    probe += 1;
5606                    match self.tokens.get(probe) {
5607                        Some(Token::Null) => probe += 1,
5608                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5609                        _ => return Ok(()),
5610                    }
5611                    match self.tokens.get(probe) {
5612                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5613                        _ => return Ok(()),
5614                    }
5615                    self.pos = probe;
5616                    attrs.strict = strict;
5617                }
5618                "security" | "external" => {
5619                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5620                    let mut probe = self.pos + 1;
5621                    if word == "external" {
5622                        match self.tokens.get(probe) {
5623                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5624                                probe += 1;
5625                            }
5626                            _ => return Ok(()),
5627                        }
5628                    }
5629                    let definer = match self.tokens.get(probe) {
5630                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5631                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5632                        _ => return Ok(()),
5633                    };
5634                    self.pos = probe + 1;
5635                    attrs.security_definer = definer;
5636                }
5637                "parallel" => {
5638                    let level = match self.tokens.get(self.pos + 1) {
5639                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5640                            FunctionParallel::Safe
5641                        }
5642                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5643                            FunctionParallel::Restricted
5644                        }
5645                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5646                            FunctionParallel::Unsafe
5647                        }
5648                        _ => return Ok(()),
5649                    };
5650                    self.pos += 2;
5651                    attrs.parallel = level;
5652                }
5653                "cost" | "rows" => {
5654                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5655                        return Ok(());
5656                    };
5657                    self.pos += 2;
5658                    if word == "cost" {
5659                        attrs.cost = Some(n);
5660                    } else {
5661                        attrs.rows = Some(n);
5662                    }
5663                }
5664                _ => return Ok(()),
5665            }
5666        }
5667    }
5668
5669    /// The numeric literal at `idx`, if there is one.
5670    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5671        match self.tokens.get(idx)? {
5672            Token::Integer(n) => Some(*n as f64),
5673            Token::Float(f) => Some(*f),
5674            Token::Numeric(t) => t.parse::<f64>().ok(),
5675            _ => None,
5676        }
5677    }
5678
5679    /// Closing `)`-terminated argument list. v7.12.4 commonly
5680    /// sees the empty `()`; typed args round-trip but the
5681    /// executor (yet) doesn't invoke them.
5682    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5683    /// it away, which is what PG does with one on a function parameter.
5684    fn skip_type_modifier(&mut self) {
5685        if !matches!(self.peek(), Token::LParen) {
5686            return;
5687        }
5688        // Only a numeric modifier — anything else is not one, and eating
5689        // it would swallow real grammar.
5690        let mut i = self.pos + 1;
5691        let mut seen_number = false;
5692        loop {
5693            match self.tokens.get(i) {
5694                Some(Token::Integer(_)) => seen_number = true,
5695                Some(Token::Comma) => {}
5696                Some(Token::RParen) => break,
5697                _ => return,
5698            }
5699            i += 1;
5700        }
5701        if !seen_number {
5702            return;
5703        }
5704        while self.pos <= i {
5705            self.advance();
5706        }
5707    }
5708
5709    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5710        let mut args: Vec<FunctionArg> = Vec::new();
5711        if matches!(self.peek(), Token::RParen) {
5712            self.advance();
5713            return Ok(args);
5714        }
5715        loop {
5716            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5717            // a reserved token; OUT / INOUT are bare idents.
5718            let mode = if matches!(self.peek(), Token::In) {
5719                self.advance();
5720                FunctionArgMode::In
5721            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5722            {
5723                self.advance();
5724                FunctionArgMode::Out
5725            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5726            {
5727                self.advance();
5728                FunctionArgMode::InOut
5729            } else {
5730                FunctionArgMode::In
5731            };
5732            // Optional name. The next token is either a name
5733            // (followed by a type ident) or the type itself.
5734            // Disambiguate by peeking ahead: if the token after
5735            // the next ident is also an ident, we treat the
5736            // first as the name.
5737            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5738            // the comma or paren, then decide. Reading at most two of
5739            // them could not spell `x double precision` at all, and
5740            // silently mis-read the bare `double precision` as a
5741            // parameter named "double" — which is what made the same
5742            // signature key two different ways.
5743            let (name, ty_token) = {
5744                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5745                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5746                    words.push(self.expect_ident_like()?);
5747                }
5748                // v7.39 (round 344) — a length / precision modifier on the
5749                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5750                // accepts it and DROPS it — `pg_get_function_arguments`
5751                // reports plain `character varying` / `numeric`, measured on
5752                // 18.4 — but SPG raised `syntax error at or near "("`,
5753                // because the modifier's parens were never consumed.
5754                self.skip_type_modifier();
5755                // r1049 — `f(v bigint[])`. The array suffix parsed in
5756                // the column position, the cast position and (r1038)
5757                // the RETURNS position, but not here: the fifth
5758                // member of the same family, reported by sentori as
5759                // presumably the same code. It is now.
5760                let array_suffix = self.consume_array_suffix();
5761                let whole = words.join(" ");
5762                let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5763                {
5764                    (Some(words[0].clone()), words[1..].join(" "))
5765                } else {
5766                    (None, whole)
5767                };
5768                ty_token.push_str(&array_suffix);
5769                (name, ty_token)
5770            };
5771            // Type — try to map to ColumnTypeName, else Raw.
5772            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5773                Some(t) => FunctionArgType::Typed(t),
5774                None => FunctionArgType::Raw(ty_token),
5775            };
5776            args.push(FunctionArg { mode, name, ty });
5777            match self.peek() {
5778                Token::Comma => {
5779                    self.advance();
5780                    continue;
5781                }
5782                Token::RParen => {
5783                    self.advance();
5784                    return Ok(args);
5785                }
5786                other => {
5787                    return Err(self.err(alloc::format!(
5788                        "expected , or ) in function arg list, got {other:?}"
5789                    )));
5790                }
5791            }
5792        }
5793    }
5794
5795    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5796        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5797        // function whose row shape is named inline.
5798        if matches!(self.peek(), Token::Table)
5799            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5800        {
5801            self.advance(); // TABLE
5802            self.advance(); // (
5803            let mut cols: Vec<String> = Vec::new();
5804            loop {
5805                let cname = self.expect_ident_like()?;
5806                let mut ty: Vec<String> = Vec::new();
5807                loop {
5808                    match self.peek() {
5809                        Token::Comma | Token::RParen | Token::Eof => break,
5810                        _ => {}
5811                    }
5812                    match self.advance() {
5813                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5814                        other => {
5815                            if let Some(w) = unreserved_keyword_text(&other) {
5816                                ty.push(w);
5817                            }
5818                        }
5819                    }
5820                }
5821                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5822                if matches!(self.peek(), Token::Comma) {
5823                    self.advance();
5824                } else {
5825                    break;
5826                }
5827            }
5828            if matches!(self.peek(), Token::RParen) {
5829                self.advance();
5830            }
5831            return Ok(FunctionReturn::Other(alloc::format!(
5832                "TABLE({})",
5833                cols.join(", ")
5834            )));
5835        }
5836        let ident = self.expect_ident_like()?;
5837        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5838        if ident.eq_ignore_ascii_case("setof") {
5839            let inner = self.expect_ident_like()?;
5840            let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5841            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5842        }
5843        if ident.eq_ignore_ascii_case("trigger") {
5844            return Ok(FunctionReturn::Trigger);
5845        }
5846        if ident.eq_ignore_ascii_case("void") {
5847            return Ok(FunctionReturn::Void);
5848        }
5849        // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5850        // RETURN position did not, so the `[` was a syntax error and the
5851        // whole migration stopped. sentori worked around it by returning
5852        // zero-padded text.
5853        let suffix = self.consume_array_suffix();
5854        if !suffix.is_empty() {
5855            return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5856        }
5857        match map_type_ident_to_column_type_name(&ident) {
5858            Some(t) => Ok(FunctionReturn::Type(t)),
5859            None => Ok(FunctionReturn::Other(ident)),
5860        }
5861    }
5862
5863    /// Consume any `[]` / `[N]` array markers after a type name and give
5864    /// back their text. Empty when there are none.
5865    fn consume_array_suffix(&mut self) -> String {
5866        let mut out = String::new();
5867        while matches!(self.peek(), Token::LBracket) {
5868            self.advance();
5869            // `[N]` is accepted and, as in PG, the length is not enforced.
5870            if let Token::Integer(n) = self.peek().clone() {
5871                self.advance();
5872                out.push_str(&alloc::format!("[{n}]"));
5873            } else {
5874                out.push_str("[]");
5875            }
5876            if matches!(self.peek(), Token::RBracket) {
5877                self.advance();
5878            }
5879        }
5880        out
5881    }
5882
5883    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5884        match self.peek() {
5885            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5886                self.advance();
5887                let lang = self.expect_ident_like()?;
5888                Ok(Some(lang.to_ascii_lowercase()))
5889            }
5890            _ => Ok(None),
5891        }
5892    }
5893
5894    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5895    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5896    /// (expr)]*`. The `DOMAIN` keyword has already been
5897    /// consumed. PG allows the trailing constraints in any
5898    /// order; we approximate with a small loop.
5899    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5900        let name = self.expect_ident_like()?;
5901        // Optional `AS`.
5902        if matches!(self.peek(), Token::As) {
5903            self.advance();
5904        }
5905        // v7.39 (round 259) — keep the raw type NAME when the base is not
5906        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5907        // parent domain.
5908        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _, _, _) =
5909            self.parse_type_with_implied_flags()?;
5910        let mut default: Option<Expr> = None;
5911        let mut not_null = false;
5912        let mut checks: Vec<Expr> = Vec::new();
5913        loop {
5914            match self.peek() {
5915                Token::Default => {
5916                    if default.is_some() {
5917                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5918                    }
5919                    self.advance();
5920                    default = Some(self.parse_expr(0)?);
5921                }
5922                Token::Not => {
5923                    self.advance();
5924                    if !matches!(self.peek(), Token::Null) {
5925                        return Err(self.err(alloc::format!(
5926                            "expected NULL after NOT in DOMAIN, got {:?}",
5927                            self.peek()
5928                        )));
5929                    }
5930                    self.advance();
5931                    not_null = true;
5932                }
5933                Token::Null => {
5934                    self.advance();
5935                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5936                    // is the default-nullable marker (PG accepts it),
5937                    // but AFTER a NOT NULL it is a conflict PG refuses
5938                    // (`conflicting NULL/NOT NULL constraints`,
5939                    // PG18-measured); the old arm no-opped both ways.
5940                    if not_null {
5941                        return Err(self.err(alloc::string::String::from(
5942                            "conflicting NULL/NOT NULL constraints",
5943                        )));
5944                    }
5945                }
5946                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5947                    self.advance();
5948                    if !matches!(self.peek(), Token::LParen) {
5949                        return Err(self.err(alloc::format!(
5950                            "expected '(' after CHECK in DOMAIN, got {:?}",
5951                            self.peek()
5952                        )));
5953                    }
5954                    self.advance();
5955                    let expr = self.parse_expr(0)?;
5956                    if !matches!(self.peek(), Token::RParen) {
5957                        return Err(self.err(alloc::format!(
5958                            "expected ')' after CHECK expr, got {:?}",
5959                            self.peek()
5960                        )));
5961                    }
5962                    self.advance();
5963                    checks.push(expr);
5964                }
5965                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5966                // prefix on the constraint; we drop the name and
5967                // recurse into the constraint parsing.
5968                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5969                    self.advance();
5970                    let _ = self.expect_ident_like()?;
5971                }
5972                _ => break,
5973            }
5974        }
5975        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5976            name,
5977            base_type,
5978            base_domain: base_user_ref,
5979            default,
5980            not_null,
5981            checks,
5982        }))
5983    }
5984
5985    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5986    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5987    /// consumed.
5988    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5989        let name = self.expect_ident_like()?;
5990        // Required `AS`.
5991        if !matches!(self.peek(), Token::As) {
5992            return Err(self.err(alloc::format!(
5993                "expected AS after CREATE TYPE {name:?}, got {:?}",
5994                self.peek()
5995            )));
5996        }
5997        self.advance();
5998        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5999        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
6000        // on the next token: `(` = composite, ident `ENUM` = enum.
6001        if matches!(self.peek(), Token::LParen) {
6002            self.advance();
6003            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
6004            let mut field_user_types: Vec<Option<String>> = Vec::new();
6005            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
6006            // is legal PG (an attribute-less composite; measured — the old
6007            // e2e note claimed PG requires at least one attribute).
6008            if matches!(self.peek(), Token::RParen) {
6009                self.advance();
6010                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
6011                    name,
6012                    kind: crate::ast::TypeKind::Composite {
6013                        fields,
6014                        field_user_types,
6015                    },
6016                }));
6017            }
6018            loop {
6019                let field_name = self.expect_ident_like()?;
6020                // v7.39 (round 264) — keep the raw type name when it is not
6021                // a builtin: that is how a NESTED composite field records
6022                // which composite it holds.
6023                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _, _, _) =
6024                    self.parse_type_with_implied_flags()?;
6025                fields.push((field_name, field_type));
6026                field_user_types.push(field_user_ref);
6027                if matches!(self.peek(), Token::Comma) {
6028                    self.advance();
6029                    continue;
6030                }
6031                if matches!(self.peek(), Token::RParen) {
6032                    self.advance();
6033                    break;
6034                }
6035                return Err(self.err(alloc::format!(
6036                    "expected , or ) in composite field list, got {:?}",
6037                    self.peek()
6038                )));
6039            }
6040            if fields.is_empty() {
6041                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
6042            }
6043            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
6044                name,
6045                kind: crate::ast::TypeKind::Composite {
6046                    fields,
6047                    field_user_types,
6048                },
6049            }));
6050        }
6051        // Required `ENUM` ident.
6052        let kind_ident = match self.peek().clone() {
6053            Token::Ident(s) | Token::QuotedIdent(s) => s,
6054            other => {
6055                return Err(self.err(alloc::format!(
6056                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
6057                )));
6058            }
6059        };
6060        if !kind_ident.eq_ignore_ascii_case("enum") {
6061            return Err(self.err(alloc::format!(
6062                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
6063            )));
6064        }
6065        self.advance();
6066        if !matches!(self.peek(), Token::LParen) {
6067            return Err(self.err(alloc::format!(
6068                "expected '(' after ENUM, got {:?}",
6069                self.peek()
6070            )));
6071        }
6072        self.advance();
6073        let mut labels: Vec<String> = Vec::new();
6074        loop {
6075            match self.peek().clone() {
6076                Token::String(s) => {
6077                    self.advance();
6078                    labels.push(s);
6079                }
6080                other => {
6081                    return Err(
6082                        self.err(alloc::format!("expected enum label string, got {other:?}"))
6083                    );
6084                }
6085            }
6086            if matches!(self.peek(), Token::Comma) {
6087                self.advance();
6088                continue;
6089            }
6090            if matches!(self.peek(), Token::RParen) {
6091                self.advance();
6092                break;
6093            }
6094            return Err(self.err(alloc::format!(
6095                "expected , or ) in ENUM label list, got {:?}",
6096                self.peek()
6097            )));
6098        }
6099        if labels.is_empty() {
6100            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
6101        }
6102        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
6103            name,
6104            kind: crate::ast::TypeKind::Enum { labels },
6105        }))
6106    }
6107
6108    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
6109    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
6110    /// The `CREATE MATERIALIZED VIEW` keywords have already been
6111    /// consumed.
6112    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
6113        let if_not_exists = self.parse_if_not_exists();
6114        let name = self.expect_ident_like()?;
6115        let mut columns: Vec<String> = Vec::new();
6116        if matches!(self.peek(), Token::LParen) {
6117            self.advance();
6118            loop {
6119                let c = self.expect_ident_like()?;
6120                columns.push(c);
6121                if matches!(self.peek(), Token::Comma) {
6122                    self.advance();
6123                    continue;
6124                }
6125                if matches!(self.peek(), Token::RParen) {
6126                    self.advance();
6127                    break;
6128                }
6129                return Err(self.err(alloc::format!(
6130                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
6131                    self.peek()
6132                )));
6133            }
6134        }
6135        if !matches!(self.peek(), Token::As) {
6136            return Err(self.err(alloc::format!(
6137                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
6138                self.peek()
6139            )));
6140        }
6141        self.advance();
6142        // v7.39 (round 151) — a WITH-headed body is legal (read-only
6143        // CTEs only; the engine rejects data-modifying ones with PG's
6144        // message). A trailing `WITH [NO] DATA` can't START the body,
6145        // so WITH here heads the query.
6146        let body = if self.peek_is_with_kw() {
6147            self.advance();
6148            self.parse_nested_with_select()?
6149        } else {
6150            let body_stmt = self.parse_select_stmt()?;
6151            let Statement::Select(body) = body_stmt else {
6152                return Err(self.err(alloc::format!(
6153                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
6154                )));
6155            };
6156            body
6157        };
6158        // Optional trailing `WITH [NO] DATA`.
6159        let with_data = self.parse_optional_with_data(true)?;
6160        Ok(Statement::CreateMaterializedView(
6161            crate::ast::CreateMaterializedViewStatement {
6162                temporary: false,
6163                name,
6164                if_not_exists,
6165                columns,
6166                body,
6167                with_data,
6168                as_plain_table: false,
6169            },
6170        ))
6171    }
6172
6173    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
6174    /// `default_when_absent` is what to return if the tail is
6175    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
6176    /// WITH DATA).
6177    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
6178        let save = self.pos;
6179        // `WITH` is an Ident (not reserved in the lexer).
6180        let is_with = match self.peek() {
6181            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
6182            _ => false,
6183        };
6184        if !is_with {
6185            return Ok(default_when_absent);
6186        }
6187        self.advance();
6188        // Optional `NO`.
6189        let mut with_data = true;
6190        let is_no = match self.peek() {
6191            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
6192            _ => false,
6193        };
6194        if is_no {
6195            self.advance();
6196            with_data = false;
6197        }
6198        // Required `DATA` ident.
6199        let is_data = match self.peek() {
6200            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
6201            _ => false,
6202        };
6203        if is_data {
6204            self.advance();
6205            Ok(with_data)
6206        } else {
6207            // Caller's WITH wasn't WITH-DATA — rewind so the outer
6208            // parser can interpret it.
6209            self.pos = save;
6210            Ok(default_when_absent)
6211        }
6212    }
6213
6214    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
6215    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
6216    /// All keyword prefixes have already been consumed; the flags
6217    /// say which were present.
6218    fn parse_create_view_after_keyword(
6219        &mut self,
6220        or_replace: bool,
6221        _materialized_unused: bool,
6222        temporary: bool,
6223    ) -> Result<Statement, ParseError> {
6224        let if_not_exists = self.parse_if_not_exists();
6225        let name = self.expect_ident_like()?;
6226        // Optional `(col, col, …)` rename list.
6227        let mut columns: Vec<String> = Vec::new();
6228        if matches!(self.peek(), Token::LParen) {
6229            self.advance();
6230            loop {
6231                let c = self.expect_ident_like()?;
6232                columns.push(c);
6233                if matches!(self.peek(), Token::Comma) {
6234                    self.advance();
6235                    continue;
6236                }
6237                if matches!(self.peek(), Token::RParen) {
6238                    self.advance();
6239                    break;
6240                }
6241                return Err(self.err(alloc::format!(
6242                    "expected , or ) in VIEW column list, got {:?}",
6243                    self.peek()
6244                )));
6245            }
6246        }
6247        // Required `AS`.
6248        if !matches!(self.peek(), Token::As) {
6249            return Err(self.err(alloc::format!(
6250                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
6251                self.peek()
6252            )));
6253        }
6254        self.advance();
6255        // Body: a regular SELECT statement. v7.39 (round 151) — a
6256        // WITH-headed body is legal too (read-only CTEs only; the
6257        // engine rejects data-modifying ones with PG's message).
6258        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
6259        // with the check-option clause, so WITH here heads the query.
6260        let body = if self.peek_is_with_kw() {
6261            self.advance();
6262            self.parse_nested_with_select()?
6263        } else {
6264            let body_stmt = self.parse_select_stmt()?;
6265            let Statement::Select(body) = body_stmt else {
6266                return Err(self.err(alloc::format!(
6267                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
6268                )));
6269            };
6270            body
6271        };
6272        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
6273        // The SELECT parser stops before a trailing WITH, so it lands here.
6274        let check_option = if matches!(self.peek(),
6275            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
6276        {
6277            self.advance(); // WITH
6278            let opt = match self.peek() {
6279                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6280                    self.advance();
6281                    crate::ast::ViewCheckOption::Local
6282                }
6283                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6284                    self.advance();
6285                    crate::ast::ViewCheckOption::Cascaded
6286                }
6287                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6288                _ => crate::ast::ViewCheckOption::Cascaded,
6289            };
6290            if !matches!(self.peek(),
6291                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6292            {
6293                return Err(self.err(alloc::format!(
6294                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6295                    self.peek()
6296                )));
6297            }
6298            self.advance(); // CHECK
6299            if !matches!(self.peek(),
6300                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6301            {
6302                return Err(self.err(alloc::format!(
6303                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6304                    self.peek()
6305                )));
6306            }
6307            self.advance(); // OPTION
6308            Some(opt)
6309        } else {
6310            None
6311        };
6312        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6313            name,
6314            or_replace,
6315            if_not_exists,
6316            temporary,
6317            columns,
6318            body,
6319            check_option,
6320        }))
6321    }
6322
6323    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6324    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6325    /// consumed; `temporary` carries whether TEMPORARY was seen.
6326    fn parse_create_sequence_after_keyword(
6327        &mut self,
6328        temporary: bool,
6329    ) -> Result<Statement, ParseError> {
6330        let if_not_exists = self.parse_if_not_exists();
6331        let name = self.expect_ident_like()?;
6332        // Optional `AS data_type`.
6333        let data_type = if matches!(self.peek(), Token::As) {
6334            self.advance();
6335            Some(self.parse_sequence_data_type()?)
6336        } else {
6337            None
6338        };
6339        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6340        Ok(Statement::CreateSequence(
6341            crate::ast::CreateSequenceStatement {
6342                name,
6343                if_not_exists,
6344                temporary,
6345                data_type,
6346                options,
6347            },
6348        ))
6349    }
6350
6351    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6352    /// already been consumed; this is reached after `SEQUENCE`.
6353    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6354    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6355        use crate::ast::AlterDomainAction as A;
6356        let name = self.expect_ident_like()?;
6357        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6358        let kw = match self.peek() {
6359            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6360            Token::Drop => alloc::string::String::from("drop"),
6361            Token::Default => alloc::string::String::from("default"),
6362            other => {
6363                return Err(self.err(alloc::format!(
6364                    "expected an ALTER DOMAIN action, got {other:?}"
6365                )));
6366            }
6367        };
6368        let action = match kw.as_str() {
6369            "add" => {
6370                self.advance();
6371                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6372                {
6373                    self.advance();
6374                    Some(self.expect_ident_like()?)
6375                } else {
6376                    None
6377                };
6378                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6379                    return Err(self.err(alloc::format!(
6380                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6381                        self.peek()
6382                    )));
6383                }
6384                self.advance();
6385                if !matches!(self.peek(), Token::LParen) {
6386                    return Err(self.err("expected '(' after CHECK".into()));
6387                }
6388                self.advance();
6389                let check = self.parse_expr(0)?;
6390                if !matches!(self.peek(), Token::RParen) {
6391                    return Err(self.err("expected ')' after CHECK expression".into()));
6392                }
6393                self.advance();
6394                A::AddConstraint { name: cname, check }
6395            }
6396            "drop" => {
6397                self.advance();
6398                match self.peek() {
6399                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6400                        self.advance();
6401                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6402                        {
6403                            self.advance();
6404                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6405                            {
6406                                return Err(self.err("expected EXISTS after IF".into()));
6407                            }
6408                            self.advance();
6409                            true
6410                        } else {
6411                            false
6412                        };
6413                        let cn = self.expect_ident_like()?;
6414                        A::DropConstraint {
6415                            name: cn,
6416                            if_exists,
6417                        }
6418                    }
6419                    Token::Default => {
6420                        self.advance();
6421                        A::DropDefault
6422                    }
6423                    Token::Not => {
6424                        self.advance();
6425                        if !matches!(self.peek(), Token::Null) {
6426                            return Err(self.err("expected NULL after NOT".into()));
6427                        }
6428                        self.advance();
6429                        A::DropNotNull
6430                    }
6431                    other => {
6432                        return Err(self.err(alloc::format!(
6433                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6434                        )));
6435                    }
6436                }
6437            }
6438            "set" => {
6439                self.advance();
6440                match self.peek() {
6441                    Token::Default => {
6442                        self.advance();
6443                        A::SetDefault(self.parse_expr(0)?)
6444                    }
6445                    Token::Not => {
6446                        self.advance();
6447                        if !matches!(self.peek(), Token::Null) {
6448                            return Err(self.err("expected NULL after NOT".into()));
6449                        }
6450                        self.advance();
6451                        A::SetNotNull
6452                    }
6453                    other => {
6454                        return Err(self.err(alloc::format!(
6455                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6456                        )));
6457                    }
6458                }
6459            }
6460            "rename" => {
6461                self.advance();
6462                if !matches!(self.peek(), Token::To) {
6463                    return Err(self.err("expected TO after RENAME".into()));
6464                }
6465                self.advance();
6466                A::RenameTo(self.expect_ident_like()?)
6467            }
6468            other => {
6469                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6470            }
6471        };
6472        Ok(Statement::AlterDomain { name, action })
6473    }
6474
6475    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6476        let if_exists = self.parse_if_exists();
6477        let name = self.expect_ident_like()?;
6478        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6479        // the option list (PG allows only one or the other).
6480        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6481            self.advance();
6482            if matches!(self.peek(), Token::To) {
6483                self.advance();
6484            } else {
6485                self.expect_keyword_ident("to")?;
6486            }
6487            let new = self.expect_ident_like()?;
6488            return Ok(Statement::AlterSequence(
6489                crate::ast::AlterSequenceStatement {
6490                    name,
6491                    if_exists,
6492                    options: crate::ast::SequenceOptions::default(),
6493                    rename_to: Some(new),
6494                },
6495            ));
6496        }
6497        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6498        Ok(Statement::AlterSequence(
6499            crate::ast::AlterSequenceStatement {
6500                name,
6501                if_exists,
6502                options,
6503                rename_to: None,
6504            },
6505        ))
6506    }
6507
6508    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6509        let kw = self.expect_ident_like()?;
6510        match kw.to_ascii_lowercase().as_str() {
6511            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6512            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6513            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6514            other => Err(self.err(alloc::format!(
6515                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6516            ))),
6517        }
6518    }
6519
6520    fn parse_sequence_options(
6521        &mut self,
6522        allow_restart: bool,
6523    ) -> Result<crate::ast::SequenceOptions, ParseError> {
6524        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6525        let mut opts = SequenceOptions::default();
6526        #[allow(clippy::while_let_loop)]
6527        loop {
6528            // Match an ident; stop at any non-ident token (sentinel,
6529            // semicolon, end of statement).
6530            let kw_lc = match self.peek() {
6531                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6532                _ => break,
6533            };
6534            match kw_lc.as_str() {
6535                "increment" => {
6536                    self.advance();
6537                    // Optional BY.
6538                    if self.peek_is_by() {
6539                        self.advance();
6540                    }
6541                    opts.increment = Some(self.expect_signed_int()?);
6542                }
6543                "minvalue" => {
6544                    self.advance();
6545                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6546                }
6547                "maxvalue" => {
6548                    self.advance();
6549                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6550                }
6551                "no" => {
6552                    self.advance();
6553                    let what = self.expect_ident_like()?;
6554                    match what.to_ascii_lowercase().as_str() {
6555                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6556                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6557                        "cycle" => opts.cycle = Some(false),
6558                        other => {
6559                            return Err(self.err(alloc::format!(
6560                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6561                            )));
6562                        }
6563                    }
6564                }
6565                "start" => {
6566                    self.advance();
6567                    // Optional WITH.
6568                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6569                        if s.eq_ignore_ascii_case("with"))
6570                    {
6571                        self.advance();
6572                    }
6573                    opts.start = Some(self.expect_signed_int()?);
6574                }
6575                "restart" if allow_restart => {
6576                    self.advance();
6577                    // Optional WITH n; bare RESTART means restart at START.
6578                    let mut with_val: Option<i64> = None;
6579                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6580                        if s.eq_ignore_ascii_case("with"))
6581                    {
6582                        self.advance();
6583                        with_val = Some(self.expect_signed_int()?);
6584                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6585                        with_val = Some(self.expect_signed_int()?);
6586                    }
6587                    opts.restart = Some(with_val);
6588                }
6589                "cache" => {
6590                    self.advance();
6591                    opts.cache = Some(self.expect_signed_int()?);
6592                }
6593                "cycle" => {
6594                    self.advance();
6595                    opts.cycle = Some(true);
6596                }
6597                "owned" => {
6598                    self.advance();
6599                    match self.peek() {
6600                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6601                            self.advance();
6602                        }
6603                        other => {
6604                            return Err(
6605                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6606                            );
6607                        }
6608                    }
6609                    // OWNED BY {NONE | tab.col}. Read just one ident
6610                    // (NOT expect_ident_like which would auto-strip
6611                    // a schema prefix and consume the `.col` we need).
6612                    let first = match self.advance() {
6613                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6614                        other => {
6615                            return Err(self.err(alloc::format!(
6616                                "expected identifier or NONE after OWNED BY, got {other:?}"
6617                            )));
6618                        }
6619                    };
6620                    if first.eq_ignore_ascii_case("none") {
6621                        opts.owned_by = Some(SequenceOwnedBy::None);
6622                    } else if matches!(self.peek(), Token::Dot) {
6623                        self.advance();
6624                        let second = match self.advance() {
6625                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6626                            other => {
6627                                return Err(self.err(alloc::format!(
6628                                    "expected column name after OWNED BY {first}., got {other:?}"
6629                                )));
6630                            }
6631                        };
6632                        // v7.17 dump-compat fix — pg_dump emits
6633                        // OWNED BY clauses as
6634                        // `schema.table.column` (three segments).
6635                        // If a third `.<ident>` follows, treat the
6636                        // first ident as schema (drop it; SPG is
6637                        // single-schema) and the middle / last
6638                        // pair as table.column. Otherwise it's
6639                        // the two-segment form table.column.
6640                        if matches!(self.peek(), Token::Dot) {
6641                            self.advance();
6642                            let third = match self.advance() {
6643                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6644                                other => {
6645                                    return Err(self.err(alloc::format!(
6646                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6647                                    )));
6648                                }
6649                            };
6650                            let _ = first; // schema prefix discarded
6651                            opts.owned_by = Some(SequenceOwnedBy::Column {
6652                                table: second,
6653                                column: third,
6654                            });
6655                        } else {
6656                            opts.owned_by = Some(SequenceOwnedBy::Column {
6657                                table: first,
6658                                column: second,
6659                            });
6660                        }
6661                    } else {
6662                        return Err(self.err(alloc::format!(
6663                            "expected table.column or NONE after OWNED BY, got {first:?}"
6664                        )));
6665                    }
6666                }
6667                _ => break,
6668            }
6669        }
6670        Ok(opts)
6671    }
6672
6673    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6674        let neg = if matches!(self.peek(), Token::Minus) {
6675            self.advance();
6676            true
6677        } else {
6678            false
6679        };
6680        match self.peek() {
6681            Token::Integer(n) => {
6682                let v = *n;
6683                self.advance();
6684                Ok(if neg { -v } else { v })
6685            }
6686            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6687        }
6688    }
6689
6690    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6691    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6692    /// clause is fully accepted and discarded — SPG always runs
6693    /// constraint checks immediately (single-writer model). The
6694    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6695    /// in either order (per the SQL spec they're independent),
6696    /// though pg_dump always emits them in the canonical
6697    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6698    /// Stops at the first token that isn't part of the clause.
6699    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6700        self.consume_deferrable_clauses_timed().map(|_| ())
6701    }
6702
6703    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6704    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6705    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6706    /// NOT DEFERRABLE and a circular-FK migration could not load.
6707    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6708        let mut deferrable = false;
6709        let mut initially_deferred = false;
6710        loop {
6711            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6712            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6713                self.advance();
6714                deferrable = true;
6715                if self.consume_optional_initially_clause()? {
6716                    initially_deferred = true;
6717                }
6718                continue;
6719            }
6720            // `NOT DEFERRABLE` — already worked pre-3.1.
6721            if matches!(self.peek(), Token::Not) {
6722                let look = self.tokens.get(self.pos + 1);
6723                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6724                    self.advance(); // NOT
6725                    self.advance(); // DEFERRABLE
6726                    deferrable = false;
6727                    initially_deferred = false;
6728                    let _ = self.consume_optional_initially_clause()?;
6729                    continue;
6730                }
6731                break;
6732            }
6733            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6734            // accepts this without a leading [NOT] DEFERRABLE
6735            // (the timing keyword alone). pg_dump occasionally
6736            // emits it on FK constraints that inherit timing.
6737            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6738                if self.consume_optional_initially_clause()? {
6739                    initially_deferred = true;
6740                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6741                    deferrable = true;
6742                }
6743                continue;
6744            }
6745            break;
6746        }
6747        Ok((deferrable, initially_deferred))
6748    }
6749
6750    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6751    /// next token is `INITIALLY`, consume it plus the required
6752    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6753    /// Returns true when the timing seen was `DEFERRED`.
6754    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6755        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6756            return Ok(false);
6757        }
6758        self.advance(); // INITIALLY
6759        match self.advance() {
6760            Token::Ident(s)
6761                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6762            {
6763                Ok(s.eq_ignore_ascii_case("deferred"))
6764            }
6765            other => Err(self.err(alloc::format!(
6766                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6767            ))),
6768        }
6769    }
6770
6771    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6772    /// in its entirety so the parser returns Empty without
6773    /// touching the runtime. The CREATE+PROCEDURE keywords are
6774    /// already consumed; this swallows everything from the
6775    /// procedure name through the matching `END`, including
6776    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6777    /// (DELIMITER `//` makes the script splitter forward the
6778    /// whole block as one statement), `@var` session-variable
6779    /// references, and the trailing terminator.
6780    ///
6781    /// Tracks nesting depth so:
6782    ///   BEGIN
6783    ///     IF cond THEN
6784    ///       BEGIN ... END;
6785    ///     END IF;
6786    ///   END
6787    /// terminates at the outer END.
6788    fn consume_mysql_routine_body(&mut self) {
6789        // Outer skeleton: name, (...), optional clauses, BEGIN
6790        // <body> END [;]. Scan for the first BEGIN — anything
6791        // before it is signature decoration we don't care about.
6792        // Once inside BEGIN, count up on BEGIN, down on END.
6793        let mut depth: i32 = 0;
6794        let mut started = false;
6795        loop {
6796            match self.peek().clone() {
6797                Token::Begin => {
6798                    self.advance();
6799                    depth += 1;
6800                    started = true;
6801                }
6802                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6803                    self.advance();
6804                    if started {
6805                        depth -= 1;
6806                        if depth <= 0 {
6807                            // Optional trailing ident (`END IF`,
6808                            // `END LOOP`, `END WHILE`, `END CASE`,
6809                            // `END label_name`) — eat the next
6810                            // ident if present so we don't
6811                            // mistake `END IF;` for the outer
6812                            // close.
6813                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6814                                // If the next token is one of the
6815                                // PL/SQL block-closer keywords,
6816                                // the END belongs to an inner
6817                                // block; bump depth back up.
6818                                let is_inner_close = matches!(
6819                                    self.peek(),
6820                                    Token::Ident(s) | Token::QuotedIdent(s)
6821                                        if matches!(
6822                                            s.to_ascii_lowercase().as_str(),
6823                                            "if" | "loop" | "while" | "case" | "repeat"
6824                                        )
6825                                );
6826                                if is_inner_close {
6827                                    self.advance();
6828                                    depth += 1;
6829                                    continue;
6830                                }
6831                            }
6832                            // Eat optional trailing `;`.
6833                            if matches!(self.peek(), Token::Semicolon) {
6834                                self.advance();
6835                            }
6836                            return;
6837                        }
6838                    }
6839                }
6840                Token::Eof => return,
6841                _ => {
6842                    self.advance();
6843                }
6844            }
6845        }
6846    }
6847
6848    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6849    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6850    ///
6851    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6852    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6853    ///   ident, or `ident @ ident-or-quoted-string` host form)
6854    /// * `SQL SECURITY {DEFINER|INVOKER}`
6855    ///
6856    /// Each clause may appear at most once but in any order.
6857    /// The hints are pure planner / permission metadata that
6858    /// SPG's view-rewrite engine handles uniformly; we accept
6859    /// and discard. Returns `Ok(())` once a non-clause token is
6860    /// peeked (the caller then checks for the `VIEW` keyword).
6861    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6862        loop {
6863            match self.peek().clone() {
6864                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6865                    self.advance(); // ALGORITHM
6866                    // Optional `=`. MySQL spec requires it but be
6867                    // generous.
6868                    if matches!(self.peek(), Token::Eq) {
6869                        self.advance();
6870                    }
6871                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6872                    // bare ident; unknown values still parse so
6873                    // future MySQL versions don't break.
6874                    if matches!(
6875                        self.peek(),
6876                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6877                    ) {
6878                        self.advance();
6879                    }
6880                }
6881                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6882                    self.advance(); // DEFINER
6883                    if matches!(self.peek(), Token::Eq) {
6884                        self.advance();
6885                    }
6886                    // User: quoted string, ident, OR ident @ host
6887                    // (host may itself be quoted or bare).
6888                    match self.peek().clone() {
6889                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6890                            self.advance();
6891                            // Optional `@host`.
6892                            if matches!(self.peek(), Token::At) {
6893                                self.advance();
6894                                if matches!(
6895                                    self.peek(),
6896                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6897                                ) {
6898                                    self.advance();
6899                                }
6900                            }
6901                        }
6902                        _ => {}
6903                    }
6904                }
6905                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6906                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6907                    // when followed by SECURITY — the dispatcher must
6908                    // not consume a bare `SQL` token (it's not a
6909                    // legal CREATE prefix on its own).
6910                    let save = self.pos;
6911                    self.advance(); // SQL
6912                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6913                        if s2.eq_ignore_ascii_case("security"))
6914                    {
6915                        self.advance(); // SECURITY
6916                        // DEFINER / INVOKER trailing ident.
6917                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6918                            self.advance();
6919                        }
6920                    } else {
6921                        // Not a SQL SECURITY clause — roll back and
6922                        // bail; the caller will error out cleanly.
6923                        self.pos = save;
6924                        return Ok(());
6925                    }
6926                }
6927                _ => return Ok(()),
6928            }
6929        }
6930    }
6931
6932    fn parse_if_not_exists(&mut self) -> bool {
6933        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6934        {
6935            let save = self.pos;
6936            self.advance();
6937            if matches!(self.peek(), Token::Not) {
6938                self.advance();
6939                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6940                {
6941                    self.advance();
6942                    return true;
6943                }
6944            }
6945            self.pos = save;
6946        }
6947        false
6948    }
6949
6950    fn parse_if_exists(&mut self) -> bool {
6951        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6952        {
6953            let save = self.pos;
6954            self.advance();
6955            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6956            {
6957                self.advance();
6958                return true;
6959            }
6960            self.pos = save;
6961        }
6962        false
6963    }
6964
6965    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6966    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6967    /// been consumed.
6968    fn parse_create_trigger_after_keyword(
6969        &mut self,
6970        or_replace: bool,
6971    ) -> Result<Statement, ParseError> {
6972        let name = self.expect_ident_like()?;
6973        let timing = {
6974            let ident = self.expect_ident_like()?;
6975            if ident.eq_ignore_ascii_case("before") {
6976                TriggerTiming::Before
6977            } else if ident.eq_ignore_ascii_case("after") {
6978                TriggerTiming::After
6979            } else if ident.eq_ignore_ascii_case("instead") {
6980                let next = self.expect_ident_like()?;
6981                if !next.eq_ignore_ascii_case("of") {
6982                    return Err(self.err(alloc::format!(
6983                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6984                    )));
6985                }
6986                TriggerTiming::InsteadOf
6987            } else {
6988                return Err(self.err(alloc::format!(
6989                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6990                )));
6991            }
6992        };
6993        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6994        // OR is a reserved keyword token (Token::Or), not an Ident.
6995        // v7.13.0 — after an UPDATE event we may optionally see
6996        // `OF col, col, …` (mailrs round-5 G7). Columns are
6997        // captured into `update_columns` once across the whole
6998        // events list; multiple `UPDATE OF` clauses are rejected.
6999        let mut events: Vec<TriggerEvent> = Vec::new();
7000        let mut update_columns: Vec<String> = Vec::new();
7001        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
7002        events.push(first_ev);
7003        if !first_cols.is_empty() {
7004            update_columns = first_cols;
7005        }
7006        while matches!(self.peek(), Token::Or) {
7007            self.advance();
7008            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
7009            events.push(ev);
7010            if !cols.is_empty() {
7011                if !update_columns.is_empty() {
7012                    return Err(
7013                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
7014                    );
7015                }
7016                update_columns = cols;
7017            }
7018        }
7019        // ON <table>
7020        let tok = self.peek();
7021        let Token::On = tok else {
7022            return Err(self.err(alloc::format!(
7023                "expected ON after trigger events, got {tok:?}"
7024            )));
7025        };
7026        self.advance();
7027        let table = self.expect_ident_like()?;
7028        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
7029        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
7030        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
7031        // the trigger as a plain AFTER trigger (correct for every non-deferred
7032        // use; deferral timing is not yet honoured).
7033        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7034            if s.eq_ignore_ascii_case("from"))
7035        {
7036            self.advance();
7037            let _reftable = self.expect_ident_like()?;
7038        }
7039        self.consume_optional_deferrable_clauses()?;
7040        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
7041        // keyword (Token::For); EACH / ROW / STATEMENT are bare
7042        // idents.
7043        if !matches!(self.peek(), Token::For) {
7044            return Err(self.err(alloc::format!(
7045                "expected FOR EACH ROW / STATEMENT, got {:?}",
7046                self.peek()
7047            )));
7048        }
7049        self.advance();
7050        let for_each = {
7051            let e = self.expect_ident_like()?;
7052            if !e.eq_ignore_ascii_case("each") {
7053                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
7054            }
7055            let unit = self.expect_ident_like()?;
7056            if unit.eq_ignore_ascii_case("row") {
7057                TriggerForEach::Row
7058            } else if unit.eq_ignore_ascii_case("statement") {
7059                TriggerForEach::Statement
7060            } else {
7061                return Err(self.err(alloc::format!(
7062                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
7063                )));
7064            }
7065        };
7066        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
7067        let when_condition = if matches!(self.peek(),
7068            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7069        {
7070            self.advance();
7071            Some(self.parse_paren_expr("WHEN")?)
7072        } else {
7073            None
7074        };
7075        // EXECUTE FUNCTION/PROCEDURE name(...)
7076        let exec = self.expect_ident_like()?;
7077        if !exec.eq_ignore_ascii_case("execute") {
7078            return Err(self.err(alloc::format!(
7079                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
7080            )));
7081        }
7082        let fn_or_proc = self.expect_ident_like()?;
7083        if !(fn_or_proc.eq_ignore_ascii_case("function")
7084            || fn_or_proc.eq_ignore_ascii_case("procedure"))
7085        {
7086            return Err(self.err(alloc::format!(
7087                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
7088            )));
7089        }
7090        let function = self.expect_ident_like()?;
7091        // Optional empty arg list `()`.
7092        if matches!(self.peek(), Token::LParen) {
7093            self.advance();
7094            if !matches!(self.peek(), Token::RParen) {
7095                return Err(self.err(alloc::format!(
7096                    "v7.12.4 trigger function calls take no args; got {:?}",
7097                    self.peek()
7098                )));
7099            }
7100            self.advance();
7101        }
7102        Ok(Statement::CreateTrigger(CreateTriggerStatement {
7103            name,
7104            or_replace,
7105            timing,
7106            events,
7107            table,
7108            for_each,
7109            function,
7110            update_columns,
7111            when_condition,
7112        }))
7113    }
7114
7115    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
7116    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
7117    fn parse_create_rule_after_keyword(
7118        &mut self,
7119        or_replace: bool,
7120    ) -> Result<Statement, ParseError> {
7121        let name = self.expect_ident_like()?;
7122        if !matches!(self.peek(), Token::As) {
7123            return Err(self.err(alloc::format!(
7124                "expected AS in CREATE RULE, got {:?}",
7125                self.peek()
7126            )));
7127        }
7128        self.advance();
7129        if !matches!(self.peek(), Token::On) {
7130            return Err(self.err(alloc::format!(
7131                "expected ON in CREATE RULE, got {:?}",
7132                self.peek()
7133            )));
7134        }
7135        self.advance();
7136        let event = self.parse_rule_event()?;
7137        if !matches!(self.peek(), Token::To)
7138            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
7139        {
7140            return Err(self.err(alloc::format!(
7141                "expected TO after rule event, got {:?}",
7142                self.peek()
7143            )));
7144        }
7145        self.advance();
7146        let table = self.expect_ident_like()?;
7147        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
7148        let when_condition = if matches!(self.peek(), Token::Where) {
7149            self.advance();
7150            Some(self.parse_expr(0)?)
7151        } else {
7152            None
7153        };
7154        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
7155        {
7156            return Err(self.err(alloc::format!(
7157                "expected DO in CREATE RULE, got {:?}",
7158                self.peek()
7159            )));
7160        }
7161        self.advance();
7162        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
7163        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
7164        {
7165            self.advance();
7166            true
7167        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
7168            self.advance();
7169            false
7170        } else {
7171            false
7172        };
7173        // `NOTHING` | `( cmd; … )` | `cmd`.
7174        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
7175        {
7176            self.advance();
7177            Vec::new()
7178        } else if matches!(self.peek(), Token::LParen) {
7179            self.advance();
7180            let mut cmds = Vec::new();
7181            loop {
7182                cmds.push(self.parse_one_statement()?);
7183                if matches!(self.peek(), Token::Semicolon) {
7184                    self.advance();
7185                    if matches!(self.peek(), Token::RParen) {
7186                        break;
7187                    }
7188                    continue;
7189                }
7190                break;
7191            }
7192            if !matches!(self.peek(), Token::RParen) {
7193                return Err(self.err(alloc::format!(
7194                    "expected ) closing the CREATE RULE command list, got {:?}",
7195                    self.peek()
7196                )));
7197            }
7198            self.advance();
7199            cmds
7200        } else {
7201            alloc::vec![self.parse_one_statement()?]
7202        };
7203        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
7204            name,
7205            or_replace,
7206            event,
7207            table,
7208            instead,
7209            when_condition,
7210            commands,
7211        }))
7212    }
7213
7214    /// v7.39 (round 139) — a rule event keyword → uppercase string.
7215    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
7216        if matches!(self.peek(), Token::Insert) {
7217            self.advance();
7218            return Ok(alloc::string::String::from("INSERT"));
7219        }
7220        if matches!(self.peek(), Token::Select) {
7221            self.advance();
7222            return Ok(alloc::string::String::from("SELECT"));
7223        }
7224        match self.peek() {
7225            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7226                self.advance();
7227                Ok(alloc::string::String::from("UPDATE"))
7228            }
7229            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7230                self.advance();
7231                Ok(alloc::string::String::from("DELETE"))
7232            }
7233            other => Err(self.err(alloc::format!(
7234                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
7235            ))),
7236        }
7237    }
7238
7239    /// v7.13.0 — parse one trigger event, then optionally consume
7240    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
7241    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
7242    fn parse_trigger_event_with_optional_of(
7243        &mut self,
7244    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
7245        let ev = self.parse_trigger_event()?;
7246        if !matches!(ev, TriggerEvent::Update) {
7247            return Ok((ev, Vec::new()));
7248        }
7249        // `OF` is a bare ident.
7250        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
7251            return Ok((ev, Vec::new()));
7252        }
7253        self.advance(); // OF
7254        let mut cols: Vec<String> = Vec::new();
7255        loop {
7256            cols.push(self.expect_ident_like()?);
7257            if matches!(self.peek(), Token::Comma) {
7258                self.advance();
7259                continue;
7260            }
7261            break;
7262        }
7263        if cols.is_empty() {
7264            return Err(
7265                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
7266            );
7267        }
7268        Ok((ev, cols))
7269    }
7270
7271    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
7272    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
7273    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
7274    /// inside the body.
7275    /// Called by [`parse_plpgsql_body`] after the body's tokens
7276    /// have been lexed into this temporary parser.
7277    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7278        // v7.12.6 — optional DECLARE prelude.
7279        let declarations = if matches!(
7280            self.peek(),
7281            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7282        ) {
7283            self.advance();
7284            self.parse_plpgsql_declare_block()?
7285        } else {
7286            Vec::new()
7287        };
7288        // BEGIN keyword (PL/pgSQL — distinct from the SQL
7289        // `BEGIN` transaction-start, but we can reuse the
7290        // reserved Token::Begin since the body is a separate
7291        // lex/parse context).
7292        if !matches!(self.peek(), Token::Begin) {
7293            return Err(self.err(alloc::format!(
7294                "expected BEGIN at start of plpgsql block, got {:?}",
7295                self.peek()
7296            )));
7297        }
7298        self.advance();
7299        let statements = self.parse_plpgsql_stmt_list_until_end()?;
7300        // v7.37.20 (20.10) — optional EXCEPTION clause between the
7301        // body's last statement and the trailing END. When present
7302        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7303        // arms terminated by END.
7304        let exception_handlers = if matches!(
7305            self.peek(),
7306            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7307        ) {
7308            self.advance();
7309            self.parse_plpgsql_exception_handlers()?
7310        } else {
7311            Vec::new()
7312        };
7313        Ok(PlPgSqlBlock {
7314            declarations,
7315            statements,
7316            exception_handlers,
7317        })
7318    }
7319
7320    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7321    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7322    fn parse_plpgsql_exception_handlers(
7323        &mut self,
7324    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7325        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7326        loop {
7327            // Stop at END — the block-level trailing END LOOP / END;
7328            // is handled by the caller.
7329            if matches!(
7330                self.peek(),
7331                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7332            ) {
7333                return Ok(out);
7334            }
7335            // WHEN <cond> [OR <cond>]* THEN <body>
7336            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7337            {
7338                return Err(self.err(alloc::format!(
7339                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
7340                    self.peek()
7341                )));
7342            }
7343            self.advance();
7344            let mut conditions: Vec<String> = Vec::new();
7345            conditions.push(self.expect_ident_like()?);
7346            while matches!(self.peek(), Token::Or) {
7347                self.advance();
7348                conditions.push(self.expect_ident_like()?);
7349            }
7350            let then_kw = self.expect_ident_like()?;
7351            if !then_kw.eq_ignore_ascii_case("then") {
7352                return Err(self.err(alloc::format!(
7353                    "expected THEN after WHEN condition list, got {then_kw:?}"
7354                )));
7355            }
7356            let body = self.parse_plpgsql_stmt_list_until_end()?;
7357            out.push(crate::ast::ExceptionHandler { conditions, body });
7358        }
7359    }
7360
7361    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7362    /// prelude. Caller has already consumed `DECLARE`. We stop
7363    /// reading entries when we hit `BEGIN`.
7364    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7365        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7366        loop {
7367            if matches!(self.peek(), Token::Begin) {
7368                return Ok(out);
7369            }
7370            let name = self.expect_ident_like()?;
7371            // v7.37.20 (20.7) — type inference: if the next token is
7372            // `:=` or `=` (no explicit type), infer from the default
7373            // expression. Otherwise the ident that follows is the
7374            // declared type.
7375            //
7376            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7377            // (PG-standard). SPG parse-accepts and treats identically
7378            // to inference — the eventual runtime value determines
7379            // the local's type, which is faithful to how SPG handles
7380            // untyped locals today (see 20.7). Full compile-time
7381            // catalog lookup queues with v7.40 PL/pgSQL epic.
7382            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7383                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7384                // downstream declaration walker to type the local by
7385                // the runtime type of the default expression.
7386                FunctionArgType::Raw("_infer_".into())
7387            } else {
7388                let ty_token = self.expect_ident_like()?;
7389                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7390                // consume optional `.<ident>` qualifier + `%<KW>`
7391                // suffix. Both qualifier and suffix map to _infer_.
7392                if matches!(self.peek(), Token::Dot) {
7393                    self.advance();
7394                    let _ = self.expect_ident_like()?;
7395                }
7396                if matches!(self.peek(), Token::Percent) {
7397                    self.advance();
7398                    // Consume the trailing TYPE / ROWTYPE ident.
7399                    let _ = self.expect_ident_like()?;
7400                    FunctionArgType::Raw("_infer_".into())
7401                } else {
7402                    match map_type_ident_to_column_type_name(&ty_token) {
7403                        Some(t) => FunctionArgType::Typed(t),
7404                        None => FunctionArgType::Raw(ty_token),
7405                    }
7406                }
7407            };
7408            let default = match self.peek() {
7409                Token::ColonEq => {
7410                    self.advance();
7411                    Some(self.parse_expr(0)?)
7412                }
7413                Token::Eq => {
7414                    // PL/pgSQL also accepts `=` for the
7415                    // DECLARE default (PG treats them the same
7416                    // in this position).
7417                    self.advance();
7418                    Some(self.parse_expr(0)?)
7419                }
7420                _ => None,
7421            };
7422            // Mandatory `;` between declarations.
7423            if !matches!(self.peek(), Token::Semicolon) {
7424                return Err(self.err(alloc::format!(
7425                    "expected ; after DECLARE entry for {name:?}, got {:?}",
7426                    self.peek()
7427                )));
7428            }
7429            self.advance();
7430            out.push(PlPgSqlDeclare { name, ty, default });
7431        }
7432    }
7433
7434    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7435    /// the terminating `END;` (or `END IF;` etc — handled by the
7436    /// per-construct sub-parsers). Used by both the outer block
7437    /// and the IF/ELSE branch bodies.
7438    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7439        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7440        loop {
7441            // Allow trailing semicolons + END.
7442            while matches!(self.peek(), Token::Semicolon) {
7443                self.advance();
7444            }
7445            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7446            if matches!(
7447                self.peek(),
7448                Token::Ident(s) | Token::QuotedIdent(s)
7449                    if s.eq_ignore_ascii_case("end")
7450                        || s.eq_ignore_ascii_case("else")
7451                        || s.eq_ignore_ascii_case("elsif")
7452                        || s.eq_ignore_ascii_case("elseif")
7453                        || s.eq_ignore_ascii_case("exception")
7454                        || s.eq_ignore_ascii_case("when")
7455            ) {
7456                return Ok(statements);
7457            }
7458            // Otherwise: one statement, then expect `;` or
7459            // a block-terminator keyword.
7460            let stmt = self.parse_plpgsql_stmt()?;
7461            statements.push(stmt);
7462            match self.peek() {
7463                Token::Semicolon => {
7464                    self.advance();
7465                }
7466                Token::Ident(s) | Token::QuotedIdent(s)
7467                    if s.eq_ignore_ascii_case("end")
7468                        || s.eq_ignore_ascii_case("else")
7469                        || s.eq_ignore_ascii_case("elsif")
7470                        || s.eq_ignore_ascii_case("elseif")
7471                        || s.eq_ignore_ascii_case("exception")
7472                        || s.eq_ignore_ascii_case("when") =>
7473                {
7474                    // Final statement of the block without `;`.
7475                }
7476                other => {
7477                    return Err(self.err(alloc::format!(
7478                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7479                    )));
7480                }
7481            }
7482        }
7483    }
7484
7485    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7486        // RETURN keyword?
7487        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7488        {
7489            self.advance();
7490            return self.parse_plpgsql_return();
7491        }
7492        // v7.12.6 — IF block.
7493        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7494        {
7495            self.advance();
7496            return self.parse_plpgsql_if();
7497        }
7498        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7499        // Detected by peeking that token pos+3 is Ident("execute").
7500        if matches!(self.peek(), Token::For)
7501            && matches!(
7502                self.tokens.get(self.pos + 1),
7503                Some(Token::Ident(_) | Token::QuotedIdent(_))
7504            )
7505            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7506            && matches!(
7507                self.tokens.get(self.pos + 3),
7508                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7509            )
7510        {
7511            self.advance(); // FOR
7512            let var = self.expect_ident_like()?;
7513            self.advance(); // IN
7514            self.advance(); // EXECUTE
7515            // Prescan for LOOP at paren depth 0 so parse_expr stops
7516            // before the LOOP keyword (same trick as the bare-SELECT
7517            // ForQuery arm).
7518            let mut depth: i32 = 0;
7519            let mut loop_pos: Option<usize> = None;
7520            let mut scan = self.pos;
7521            while scan < self.tokens.len() {
7522                match self.tokens.get(scan) {
7523                    Some(Token::LParen) => depth += 1,
7524                    Some(Token::RParen) => depth -= 1,
7525                    Some(Token::Ident(s) | Token::QuotedIdent(s))
7526                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7527                    {
7528                        loop_pos = Some(scan);
7529                        break;
7530                    }
7531                    _ => {}
7532                }
7533                scan += 1;
7534            }
7535            let loop_pos = loop_pos.ok_or_else(|| {
7536                self.err(alloc::format!(
7537                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7538                ))
7539            })?;
7540            let saved_loop = self.tokens[loop_pos].clone();
7541            self.tokens[loop_pos] = Token::Semicolon;
7542            let expr_result = self.parse_expr(0);
7543            self.tokens[loop_pos] = saved_loop;
7544            let sql_expr = expr_result?;
7545            let loop_kw = self.expect_ident_like()?;
7546            if !loop_kw.eq_ignore_ascii_case("loop") {
7547                return Err(self.err(alloc::format!(
7548                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7549                )));
7550            }
7551            let body = self.parse_plpgsql_stmt_list_until_end()?;
7552            let end_kw = self.expect_ident_like()?;
7553            if !end_kw.eq_ignore_ascii_case("end") {
7554                return Err(self.err(alloc::format!(
7555                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7556                )));
7557            }
7558            let loop_kw2 = self.expect_ident_like()?;
7559            if !loop_kw2.eq_ignore_ascii_case("loop") {
7560                return Err(self.err(alloc::format!(
7561                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7562                )));
7563            }
7564            return Ok(PlPgSqlStmt::ForExecute {
7565                var,
7566                sql_expr,
7567                body,
7568            });
7569        }
7570        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7571        //
7572        // Two syntactic forms:
7573        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7574        //   FOR var IN (SELECT ...) LOOP ...
7575        //
7576        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7577        // the trailing `LOOP` keyword as a table alias, we prescan
7578        // forward to find LOOP at paren depth 0, splice a fake
7579        // Semicolon at that position (so SELECT parses cleanly),
7580        // then re-splice LOOP back in.
7581        //
7582        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7583        // LOOP directly — no scan required.
7584        if matches!(self.peek(), Token::For)
7585            && matches!(
7586                self.tokens.get(self.pos + 1),
7587                Some(Token::Ident(_) | Token::QuotedIdent(_))
7588            )
7589            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7590            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7591                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7592        {
7593            self.advance(); // FOR
7594            let var = self.expect_ident_like()?;
7595            // IN
7596            self.advance();
7597            let query = if matches!(self.peek(), Token::LParen) {
7598                // Paren-wrapped SELECT.
7599                self.advance();
7600                let inner = self.parse_select_stmt()?;
7601                let Statement::Select(q) = inner else {
7602                    return Err(self.err(alloc::format!(
7603                        "expected SELECT inside (…), got {:?}",
7604                        self.peek()
7605                    )));
7606                };
7607                if !matches!(self.peek(), Token::RParen) {
7608                    return Err(self.err(alloc::format!(
7609                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7610                        self.peek()
7611                    )));
7612                }
7613                self.advance();
7614                q
7615            } else {
7616                // Bare SELECT: prescan to find the LOOP boundary.
7617                let mut depth: i32 = 0;
7618                let mut loop_pos: Option<usize> = None;
7619                let mut scan = self.pos;
7620                while scan < self.tokens.len() {
7621                    match self.tokens.get(scan) {
7622                        Some(Token::LParen) => depth += 1,
7623                        Some(Token::RParen) => depth -= 1,
7624                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7625                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7626                        {
7627                            loop_pos = Some(scan);
7628                            break;
7629                        }
7630                        _ => {}
7631                    }
7632                    scan += 1;
7633                }
7634                let loop_pos = loop_pos.ok_or_else(|| {
7635                    self.err(alloc::format!(
7636                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7637                    ))
7638                })?;
7639                // Swap the LOOP token with a synthetic Semicolon so
7640                // parse_select_stmt stops there, then restore afterward.
7641                let saved_loop = self.tokens[loop_pos].clone();
7642                self.tokens[loop_pos] = Token::Semicolon;
7643                let parse_result = self.parse_select_stmt();
7644                self.tokens[loop_pos] = saved_loop;
7645                let inner = parse_result?;
7646                let Statement::Select(q) = inner else {
7647                    return Err(self.err(alloc::format!(
7648                        "expected SELECT after FOR <var> IN, got {:?}",
7649                        self.peek()
7650                    )));
7651                };
7652                q
7653            };
7654            let loop_kw = self.expect_ident_like()?;
7655            if !loop_kw.eq_ignore_ascii_case("loop") {
7656                return Err(self.err(alloc::format!(
7657                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7658                )));
7659            }
7660            let body = self.parse_plpgsql_stmt_list_until_end()?;
7661            let end_kw = self.expect_ident_like()?;
7662            if !end_kw.eq_ignore_ascii_case("end") {
7663                return Err(self.err(alloc::format!(
7664                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7665                )));
7666            }
7667            let loop_kw2 = self.expect_ident_like()?;
7668            if !loop_kw2.eq_ignore_ascii_case("loop") {
7669                return Err(self.err(alloc::format!(
7670                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7671                )));
7672            }
7673            return Ok(PlPgSqlStmt::ForQuery {
7674                var,
7675                query: Box::new(query),
7676                body,
7677            });
7678        }
7679        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7680        // FOR is a reserved keyword token (Token::For).
7681        if matches!(self.peek(), Token::For)
7682            && matches!(
7683                self.tokens.get(self.pos + 1),
7684                Some(Token::Ident(_) | Token::QuotedIdent(_))
7685            )
7686            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7687        {
7688            self.advance(); // FOR
7689            let var = self.expect_ident_like()?;
7690            if !matches!(self.peek(), Token::In) {
7691                return Err(self.err(alloc::format!(
7692                    "expected IN after FOR <var>, got {:?}",
7693                    self.peek()
7694                )));
7695            }
7696            self.advance();
7697            let reverse = matches!(
7698                self.peek(),
7699                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7700            );
7701            if reverse {
7702                self.advance();
7703            }
7704            let start = self.parse_expr(0)?;
7705            if !matches!(self.peek(), Token::DotDot) {
7706                return Err(self.err(alloc::format!(
7707                    "expected '..' between FOR loop bounds, got {:?}",
7708                    self.peek()
7709                )));
7710            }
7711            self.advance();
7712            let end = self.parse_expr(0)?;
7713            let loop_kw = self.expect_ident_like()?;
7714            if !loop_kw.eq_ignore_ascii_case("loop") {
7715                return Err(self.err(alloc::format!(
7716                    "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7717                )));
7718            }
7719            let body = self.parse_plpgsql_stmt_list_until_end()?;
7720            let end_kw = self.expect_ident_like()?;
7721            if !end_kw.eq_ignore_ascii_case("end") {
7722                return Err(self.err(alloc::format!(
7723                    "expected END LOOP after FOR body, got {end_kw:?}"
7724                )));
7725            }
7726            let loop_kw2 = self.expect_ident_like()?;
7727            if !loop_kw2.eq_ignore_ascii_case("loop") {
7728                return Err(self.err(alloc::format!(
7729                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7730                )));
7731            }
7732            return Ok(PlPgSqlStmt::ForRange {
7733                var,
7734                start,
7735                end,
7736                reverse,
7737                body,
7738            });
7739        }
7740        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7741        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7742        {
7743            self.advance();
7744            let body = self.parse_plpgsql_stmt_list_until_end()?;
7745            let end_kw = self.expect_ident_like()?;
7746            if !end_kw.eq_ignore_ascii_case("end") {
7747                return Err(self.err(alloc::format!(
7748                    "expected END LOOP after LOOP body, got {end_kw:?}"
7749                )));
7750            }
7751            let loop_kw = self.expect_ident_like()?;
7752            if !loop_kw.eq_ignore_ascii_case("loop") {
7753                return Err(self.err(alloc::format!(
7754                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7755                )));
7756            }
7757            return Ok(PlPgSqlStmt::Loop { body });
7758        }
7759        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7760        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7761        {
7762            self.advance();
7763            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7764            {
7765                self.advance();
7766                Some(self.parse_expr(0)?)
7767            } else {
7768                None
7769            };
7770            return Ok(PlPgSqlStmt::Exit { when });
7771        }
7772        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7773        // already-parsed Statement or a runtime-computed SQL string.
7774        // The disambiguator vs the extended-query-protocol `EXECUTE
7775        // <stmt_name>` (which is a top-level Statement, not a
7776        // plpgsql line) is that inside a DO block / trigger body the
7777        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7778        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7779        {
7780            self.advance();
7781            let sql = self.parse_expr(0)?;
7782            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7783        }
7784        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7785        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7786        {
7787            self.advance();
7788            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7789            {
7790                self.advance();
7791                Some(self.parse_expr(0)?)
7792            } else {
7793                None
7794            };
7795            return Ok(PlPgSqlStmt::Continue { when });
7796        }
7797        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7798        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7799        {
7800            self.advance();
7801            let condition = self.parse_expr(0)?;
7802            let loop_kw = self.expect_ident_like()?;
7803            if !loop_kw.eq_ignore_ascii_case("loop") {
7804                return Err(self.err(alloc::format!(
7805                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7806                )));
7807            }
7808            let body = self.parse_plpgsql_stmt_list_until_end()?;
7809            // Expect END LOOP.
7810            let end_kw = self.expect_ident_like()?;
7811            if !end_kw.eq_ignore_ascii_case("end") {
7812                return Err(self.err(alloc::format!(
7813                    "expected END LOOP after WHILE body, got {end_kw:?}"
7814                )));
7815            }
7816            let loop_kw2 = self.expect_ident_like()?;
7817            if !loop_kw2.eq_ignore_ascii_case("loop") {
7818                return Err(self.err(alloc::format!(
7819                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7820                )));
7821            }
7822            return Ok(PlPgSqlStmt::While { condition, body });
7823        }
7824        // v7.12.6 — RAISE.
7825        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7826        {
7827            self.advance();
7828            return self.parse_plpgsql_raise();
7829        }
7830        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7831        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7832        {
7833            self.advance();
7834            let condition = self.parse_expr(0)?;
7835            let message = if matches!(self.peek(), Token::Comma) {
7836                self.advance();
7837                Some(self.parse_expr(0)?)
7838            } else {
7839                None
7840            };
7841            return Ok(PlPgSqlStmt::Assert { condition, message });
7842        }
7843        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7844        //   "PERFORM is equivalent to SELECT but discards the
7845        //    result." Side effects (function calls, RAISE inside
7846        //    SQL functions, etc.) still execute. We desugar to
7847        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7848        //    existing embedded-statement path handles execution +
7849        //    result-discard cleanly. The result is naturally
7850        //    discarded because EmbeddedSql doesn't propagate row
7851        //    sets back to the plpgsql interpreter.
7852        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7853        {
7854            self.advance();
7855            // Splice a synthetic Token::Select into the stream at
7856            // the current position so parse_select_stmt parses the
7857            // remainder as a normal SELECT body. Token-stream
7858            // surgery mirrors the try_parse_plpgsql_select_into
7859            // pattern used for SELECT … INTO desugaring.
7860            self.tokens.insert(self.pos, Token::Select);
7861            let select = self.parse_select_stmt()?;
7862            let Statement::Select(s) = select else {
7863                return Err(self.err(alloc::format!(
7864                    "expected SELECT body after PERFORM, got {:?}",
7865                    self.peek()
7866                )));
7867            };
7868            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7869        }
7870        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7871        // plpgsql-specific shape (mailrs round-10 migrate-042).
7872        // PG's SELECT INTO at top-level SQL would CREATE a new
7873        // table; inside plpgsql it ASSIGNS the query result to
7874        // a local variable. We detect the INTO at paren-depth
7875        // 0 between SELECT and the statement boundary; if
7876        // found, split the token stream into "pre-INTO
7877        // projection" + "var" + "post-INTO FROM/WHERE…" and
7878        // rebuild as a SelectInto with a regular SELECT body
7879        // (no INTO clause).
7880        if matches!(self.peek(), Token::Select)
7881            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7882        {
7883            return Ok(PlPgSqlStmt::SelectInto {
7884                var: var_name,
7885                body: Box::new(select_body),
7886            });
7887        }
7888        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7889        // SELECT can appear directly inside a trigger body; we
7890        // recurse into the regular Statement parser, which will
7891        // stop at the trailing `;` (which our caller then
7892        // consumes).
7893        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7894        // also embed ALTER / CREATE / DROP statements; route
7895        // those through the same parser so the DO body parses
7896        // cleanly.
7897        if matches!(self.peek(), Token::Insert)
7898            || matches!(self.peek(), Token::Select)
7899            || matches!(self.peek(), Token::Create)
7900            || matches!(self.peek(), Token::Drop)
7901            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7902                if s.eq_ignore_ascii_case("update")
7903                    || s.eq_ignore_ascii_case("delete")
7904                    || s.eq_ignore_ascii_case("alter"))
7905        {
7906            let stmt = self.parse_one_statement()?;
7907            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7908        }
7909        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7910        // followed by `:=` and an expression.
7911        let target = self.parse_plpgsql_assign_target()?;
7912        // PL/pgSQL assignment uses `:=`. The lexer represents
7913        // this as a colon followed by `=`; check both shapes.
7914        match self.peek() {
7915            Token::ColonEq => {
7916                self.advance();
7917            }
7918            Token::Colon => {
7919                self.advance();
7920                if !matches!(self.peek(), Token::Eq) {
7921                    return Err(self.err(alloc::format!(
7922                        "expected := after plpgsql assign target, got `:` then {:?}",
7923                        self.peek()
7924                    )));
7925                }
7926                self.advance();
7927            }
7928            other => {
7929                return Err(self.err(alloc::format!(
7930                    "expected := after plpgsql assign target, got {other:?}"
7931                )));
7932            }
7933        }
7934        let value = self.parse_expr(0)?;
7935        Ok(PlPgSqlStmt::Assign { target, value })
7936    }
7937
7938    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7939    /// [ELSE body] END IF`. `IF` keyword already consumed.
7940    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7941        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7942        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7943        loop {
7944            // <expr> THEN
7945            let cond = self.parse_expr(0)?;
7946            let then_kw = self.expect_ident_like()?;
7947            if !then_kw.eq_ignore_ascii_case("then") {
7948                return Err(self.err(alloc::format!(
7949                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7950                )));
7951            }
7952            let body = self.parse_plpgsql_stmt_list_until_end()?;
7953            branches.push((cond, body));
7954            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7955            match self.peek() {
7956                Token::Ident(s) | Token::QuotedIdent(s)
7957                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7958                {
7959                    self.advance();
7960                    continue;
7961                }
7962                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7963                    self.advance();
7964                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7965                    break;
7966                }
7967                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7968                    break;
7969                }
7970                other => {
7971                    return Err(self.err(alloc::format!(
7972                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7973                    )));
7974                }
7975            }
7976        }
7977        // Expect `END IF` (the END keyword is the one we're
7978        // looking at right now).
7979        let end_kw = self.expect_ident_like()?;
7980        if !end_kw.eq_ignore_ascii_case("end") {
7981            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7982        }
7983        let if_kw = self.expect_ident_like()?;
7984        if !if_kw.eq_ignore_ascii_case("if") {
7985            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7986        }
7987        Ok(PlPgSqlStmt::If {
7988            branches,
7989            else_branch,
7990        })
7991    }
7992
7993    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7994    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7995    /// is already consumed.
7996    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7997        let lvl_ident = self.expect_ident_like()?;
7998        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7999            "notice" => RaiseLevel::Notice,
8000            "warning" => RaiseLevel::Warning,
8001            "info" => RaiseLevel::Info,
8002            "log" => RaiseLevel::Log,
8003            "debug" => RaiseLevel::Debug,
8004            "exception" => RaiseLevel::Exception,
8005            other => {
8006                return Err(self.err(alloc::format!(
8007                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
8008                )));
8009            }
8010        };
8011        // Message: required for v7.12.6. PG accepts a bare
8012        // RAISE-rethrow form (no message), reserved for future
8013        // RAISE-no-args support.
8014        let Token::String(msg) = self.peek() else {
8015            return Err(self.err(alloc::format!(
8016                "expected RAISE message string, got {:?}",
8017                self.peek()
8018            )));
8019        };
8020        let message = msg.clone();
8021        self.advance();
8022        // Optional comma-separated args (PG `%` format substitution).
8023        let mut args: Vec<Expr> = Vec::new();
8024        while matches!(self.peek(), Token::Comma) {
8025            self.advance();
8026            args.push(self.parse_expr(0)?);
8027        }
8028        Ok(PlPgSqlStmt::Raise {
8029            level,
8030            message,
8031            args,
8032        })
8033    }
8034
8035    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
8036    /// <projection> INTO <var> [FROM …]` (mailrs round-10
8037    /// migrate-042). Returns `(rebuilt_select_without_into,
8038    /// var_name)` when the pattern matches; `None` for
8039    /// regular SELECTs (those go through the embedded-SQL
8040    /// path). Token-stream surgery so the rebuilt SELECT
8041    /// parses through the regular `parse_select_stmt`.
8042    #[allow(clippy::too_many_lines)]
8043    fn try_parse_plpgsql_select_into(
8044        &mut self,
8045    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
8046        // Scan forward from `self.pos + 1` (past Token::Select)
8047        // for Token::Into at paren-depth 0, stopping at the
8048        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
8049        // end the plpgsql statement.
8050        let start = self.pos;
8051        let mut into_pos: Option<usize> = None;
8052        let mut depth: i32 = 0;
8053        let mut i = start + 1;
8054        while i < self.tokens.len() {
8055            match &self.tokens[i] {
8056                Token::LParen => depth += 1,
8057                Token::RParen => depth -= 1,
8058                Token::Semicolon if depth == 0 => break,
8059                Token::Ident(s)
8060                    if depth == 0
8061                        && (s.eq_ignore_ascii_case("end")
8062                            || s.eq_ignore_ascii_case("else")
8063                            || s.eq_ignore_ascii_case("elsif")) =>
8064                {
8065                    break;
8066                }
8067                Token::Into if depth == 0 => {
8068                    into_pos = Some(i);
8069                    break;
8070                }
8071                _ => {}
8072            }
8073            i += 1;
8074        }
8075        let Some(into_at) = into_pos else {
8076            return Ok(None);
8077        };
8078        // The token immediately after INTO must be the target
8079        // var ident; anything else (e.g. INSERT INTO table)
8080        // ruled out by the depth-0 check above. Capture it.
8081        let var = match self.tokens.get(into_at + 1) {
8082            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
8083            other => {
8084                return Err(self.err(alloc::format!(
8085                    "expected variable name after SELECT … INTO, got {other:?}"
8086                )));
8087            }
8088        };
8089        // Find the end of the plpgsql SELECT INTO statement —
8090        // same boundary rules as the depth-0 scan above.
8091        let mut end = into_at + 2;
8092        let mut depth2: i32 = 0;
8093        while end < self.tokens.len() {
8094            match &self.tokens[end] {
8095                Token::LParen => depth2 += 1,
8096                Token::RParen => depth2 -= 1,
8097                Token::Semicolon if depth2 == 0 => break,
8098                Token::Ident(s)
8099                    if depth2 == 0
8100                        && (s.eq_ignore_ascii_case("end")
8101                            || s.eq_ignore_ascii_case("else")
8102                            || s.eq_ignore_ascii_case("elsif")) =>
8103                {
8104                    break;
8105                }
8106                _ => {}
8107            }
8108            end += 1;
8109        }
8110        // Rebuild a token stream that represents the SELECT
8111        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
8112        // post-var tokens up to statement end]. Run the
8113        // regular `parse_select_stmt` against it.
8114        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
8115        for j in start..into_at {
8116            rebuilt.push(self.tokens[j].clone());
8117        }
8118        for j in (into_at + 2)..end {
8119            rebuilt.push(self.tokens[j].clone());
8120        }
8121        rebuilt.push(Token::Eof);
8122        let saved_pos = self.pos;
8123        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
8124        self.pos = 0;
8125        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
8126        if !matches!(self.peek(), Token::Select) {
8127            self.tokens = saved_tokens;
8128            self.pos = saved_pos;
8129            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
8130        }
8131        let sel = self.parse_select_stmt();
8132        self.tokens = saved_tokens;
8133        self.pos = end;
8134        let sel = sel?;
8135        let Statement::Select(body) = sel else {
8136            return Err(self.err(alloc::format!(
8137                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
8138            )));
8139        };
8140        Ok(Some((body, var)))
8141    }
8142
8143    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
8144        // v7.16.1 — read the head token DIRECTLY rather than
8145        // via `expect_ident_like`. The v7.14.0 schema-qualifier
8146        // strip (`public.t` → `t`) inside `expect_ident_like`
8147        // greedily consumes any `ident . ident` pair, which
8148        // silently turned every `NEW.col := …` /
8149        // `OLD.col := …` plpgsql assignment into a Local("col")
8150        // assignment — the head "new"/"old" was eaten as if it
8151        // were a schema name and the Dot was consumed too, so
8152        // this function's own `peek() == Token::Dot` check
8153        // below never fired. Every BEFORE trigger that rewrote
8154        // a NEW cell was a silent no-op for two major releases
8155        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
8156        // gate failures were investigated as v7.16.1 backlog.
8157        let head = match self.advance() {
8158            Token::Ident(s) | Token::QuotedIdent(s) => s,
8159            other => {
8160                return Err(self.err(alloc::format!(
8161                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
8162                )));
8163            }
8164        };
8165        if matches!(self.peek(), Token::Dot) {
8166            self.advance();
8167            let col = self.expect_ident_like()?;
8168            if head.eq_ignore_ascii_case("new") {
8169                return Ok(AssignTarget::NewColumn(col));
8170            }
8171            if head.eq_ignore_ascii_case("old") {
8172                return Ok(AssignTarget::OldColumn(col));
8173            }
8174            return Err(self.err(alloc::format!(
8175                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
8176                 got {head:?}.<col>"
8177            )));
8178        }
8179        Ok(AssignTarget::Local(head))
8180    }
8181
8182    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
8183        // RETURN NEW / OLD / NULL — bare-ident forms.
8184        match self.peek() {
8185            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
8186                self.advance();
8187                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
8188            }
8189            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
8190                self.advance();
8191                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
8192            }
8193            Token::Null => {
8194                self.advance();
8195                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8196            }
8197            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
8198            // per PL/pgSQL convention.
8199            Token::Semicolon => {
8200                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8201            }
8202            _ => {}
8203        }
8204        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
8205        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
8206        // caller-visible effect (blocks don't return sets), so we
8207        // desugar it identically to PERFORM: parse the SELECT (or
8208        // EXECUTE dynamic) as embedded SQL that runs for side
8209        // effects and discards the result. RETURN NEXT <expr>
8210        // (single-row accumulator) queues with v7.40 SETOF function
8211        // infrastructure.
8212        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
8213        // and keep going.
8214        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
8215        {
8216            self.advance();
8217            let e = self.parse_expr(0)?;
8218            return Ok(PlPgSqlStmt::ReturnNext(e));
8219        }
8220        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
8221        {
8222            self.advance();
8223            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
8224            // rows go to the set, like the static form. It used to desugar to a
8225            // bare ExecuteDynamic, whose result was DISCARDED.
8226            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
8227            {
8228                self.advance();
8229                let sql = self.parse_expr(0)?;
8230                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
8231            }
8232            // Bare RETURN QUERY <select>. If the current token is
8233            // not already SELECT (e.g., the user wrote `RETURN QUERY
8234            // <projection> FROM ...` in a shorthand — rare but PG
8235            // accepts a bare projection here), splice one in. Same
8236            // trick as PERFORM.
8237            if !matches!(self.peek(), Token::Select) {
8238                self.tokens.insert(self.pos, Token::Select);
8239            }
8240            let select = self.parse_select_stmt()?;
8241            let Statement::Select(s) = select else {
8242                return Err(self.err(alloc::format!(
8243                    "expected SELECT body after RETURN QUERY, got {:?}",
8244                    self.peek()
8245                )));
8246            };
8247            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
8248            // to an embedded side-effect SELECT whose rows were DISCARDED, which
8249            // in a SETOF function is the entire answer thrown away.
8250            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
8251        }
8252        // Fall through: parse a full expression.
8253        let e = self.parse_expr(0)?;
8254        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
8255    }
8256
8257    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
8258        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
8259        // are ident-shaped (the parser keys off case-insensitive
8260        // match — same shape used by the top-level Update / Delete
8261        // dispatchers at parse_one_statement).
8262        if matches!(self.peek(), Token::Insert) {
8263            self.advance();
8264            return Ok(TriggerEvent::Insert);
8265        }
8266        match self.peek() {
8267            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
8268                self.advance();
8269                Ok(TriggerEvent::Update)
8270            }
8271            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
8272                self.advance();
8273                Ok(TriggerEvent::Delete)
8274            }
8275            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
8276                self.advance();
8277                Ok(TriggerEvent::Truncate)
8278            }
8279            other => Err(self.err(alloc::format!(
8280                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8281            ))),
8282        }
8283    }
8284
8285    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8286    ///   - (no clause) → implicit `FOR ALL TABLES`
8287    ///   - `FOR ALL TABLES`
8288    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8289    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8290    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
8291    ///     REJECTS the bare plural (`invalid publication object list`,
8292    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
8293    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8294    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8295        let name = self.expect_ident_or_string()?;
8296        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8297        // shape so existing publications keep parsing identically.
8298        let scope = if matches!(self.peek(), Token::For) {
8299            self.advance();
8300            if matches!(self.peek(), Token::All) {
8301                self.advance();
8302                if !matches!(self.peek(), Token::Tables) {
8303                    return Err(self.err(format!(
8304                        "expected TABLES after FOR ALL, got {:?}",
8305                        self.peek()
8306                    )));
8307                }
8308                self.advance();
8309                if matches!(self.peek(), Token::Except) {
8310                    self.advance();
8311                    let tables = self.parse_publication_table_list()?;
8312                    PublicationScope::AllTablesExcept(tables)
8313                } else {
8314                    PublicationScope::AllTables
8315                }
8316            } else if matches!(self.peek(), Token::Table) {
8317                self.advance();
8318                let tables = self.parse_publication_table_list()?;
8319                PublicationScope::ForTables(tables)
8320            } else if matches!(self.peek(), Token::Tables) {
8321                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8322                // plural (`FOR TABLES t`) is REJECTED (`invalid
8323                // publication object list`); TABLES only pairs with
8324                // `IN SCHEMA`. The old arm accepted it on an
8325                // unverifiable "PG 19 accepts both" claim.
8326                self.advance();
8327                if !matches!(self.peek(), Token::In) {
8328                    return Err(self.err(alloc::string::String::from(
8329                        "invalid publication object list",
8330                    )));
8331                }
8332                self.advance();
8333                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8334                    return Err(self.err(format!(
8335                        "expected SCHEMA after FOR TABLES IN, got {:?}",
8336                        self.peek()
8337                    )));
8338                }
8339                self.advance();
8340                let schema = self.expect_ident_or_string()?;
8341                PublicationScope::TablesInSchema(schema)
8342            } else {
8343                return Err(self.err(format!(
8344                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8345                    self.peek()
8346                )));
8347            }
8348        } else {
8349            PublicationScope::AllTables
8350        };
8351        Ok(Statement::CreatePublication(CreatePublicationStatement {
8352            name,
8353            scope,
8354        }))
8355    }
8356
8357    /// v6.1.3 — Comma-separated identifier list for the publication
8358    /// FOR-clause. Requires at least one entry; empty list is a
8359    /// parse error (PG behaviour). Quoted idents are accepted; the
8360    /// names round-trip through `Display` as `quote_ident(name)`.
8361    ///
8362    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8363    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8364    /// pg_dump output. SPG's publication state today is per-table
8365    /// only (matching the pre-PG-15 surface); the col list + WHERE
8366    /// are parsed so dumps load through and the table name reaches
8367    /// `PublicationScope::ForTables`, but the filter is not enforced
8368    /// at publish time. Re-open when a customer dogfood gate
8369    /// requires per-row-filter or column-subset publish semantics
8370    /// (which gates on persistent slot state landing first, 21.12).
8371    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8372        let first = self.parse_publication_table_entry()?;
8373        let mut out = alloc::vec![first];
8374        while matches!(self.peek(), Token::Comma) {
8375            self.advance();
8376            out.push(self.parse_publication_table_entry()?);
8377        }
8378        Ok(out)
8379    }
8380
8381    /// One table entry inside a FOR TABLE clause:
8382    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8383    /// Returns just the table name; the column list + WHERE predicate
8384    /// are consumed and discarded per the parse-accept-discard
8385    /// commitment above.
8386    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8387        let name = self.expect_ident_like()?;
8388        // Optional column list — `(col, col, …)`.
8389        if matches!(self.peek(), Token::LParen) {
8390            self.advance();
8391            // Empty parens are a PG error too; require ≥ 1 column.
8392            let _ = self.expect_ident_like()?;
8393            while matches!(self.peek(), Token::Comma) {
8394                self.advance();
8395                let _ = self.expect_ident_like()?;
8396            }
8397            if !matches!(self.peek(), Token::RParen) {
8398                return Err(self.err(alloc::format!(
8399                    "expected ')' to close publication column list, got {:?}",
8400                    self.peek()
8401                )));
8402            }
8403            self.advance();
8404        }
8405        // Optional row filter — `WHERE (predicate)`.
8406        if matches!(self.peek(), Token::Where) {
8407            self.advance();
8408            if !matches!(self.peek(), Token::LParen) {
8409                return Err(self.err(alloc::format!(
8410                    "expected '(' after WHERE in publication row filter, got {:?}",
8411                    self.peek()
8412                )));
8413            }
8414            self.advance();
8415            let _ = self.parse_expr(0)?;
8416            if !matches!(self.peek(), Token::RParen) {
8417                return Err(self.err(alloc::format!(
8418                    "expected ')' to close publication WHERE filter, got {:?}",
8419                    self.peek()
8420                )));
8421            }
8422            self.advance();
8423        }
8424        Ok(name)
8425    }
8426
8427    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8428    ///                 CONNECTION '<conn>'
8429    ///                 PUBLICATION <pub> [, <pub> ...]`.
8430    ///
8431    /// The clause order is fixed (CONNECTION first, then
8432    /// PUBLICATION) to match PG. No WITH-options accepted in
8433    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8434    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8435        let name = self.expect_ident_or_string()?;
8436        if !matches!(self.peek(), Token::Connection) {
8437            return Err(self.err(format!(
8438                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8439                self.peek()
8440            )));
8441        }
8442        self.advance();
8443        let conn_str = self.expect_string_literal()?;
8444        if !matches!(self.peek(), Token::Publication) {
8445            return Err(self.err(format!(
8446                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8447                self.peek()
8448            )));
8449        }
8450        self.advance();
8451        // Reuse the publication FOR-list parser shape: at least one
8452        // identifier, comma-separated.
8453        let first = self.expect_ident_like()?;
8454        let mut publications = alloc::vec![first];
8455        while matches!(self.peek(), Token::Comma) {
8456            self.advance();
8457            publications.push(self.expect_ident_like()?);
8458        }
8459        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8460            name,
8461            conn_str,
8462            publications,
8463        }))
8464    }
8465
8466    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8467    /// All keywords after `WAIT` are bare idents in v6.1.x; no
8468    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8469    /// that fit `u64`.
8470    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8471    /// qualifier is a *namespace* the app owns (`app.user_id`,
8472    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8473    /// to discard. So parse the raw segments here instead of
8474    /// `expect_ident_like`, which strips a leading `schema.` qualifier
8475    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8476    /// a single segment and round-trip unchanged.
8477    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8478        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8479        loop {
8480            let seg = match self.advance() {
8481                Token::Ident(s) | Token::QuotedIdent(s) => s,
8482                other if unreserved_keyword_text(&other).is_some() => {
8483                    unreserved_keyword_text(&other).unwrap()
8484                }
8485                other => {
8486                    return Err(ParseError {
8487                        message: format!("expected parameter name, got {other:?}"),
8488                        token_pos: self.consumed_pos(),
8489                    });
8490                }
8491            };
8492            parts.push(seg);
8493            if matches!(self.peek(), Token::Dot) {
8494                self.advance();
8495                continue;
8496            }
8497            break;
8498        }
8499        Ok(parts.join(".").to_ascii_lowercase())
8500    }
8501
8502    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8503        Self::parse_set_value_inner(self)
8504    }
8505
8506    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8507        match self.advance() {
8508            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8509            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8510                Ok(crate::ast::SetValue::Default)
8511            }
8512            Token::Ident(s) | Token::QuotedIdent(s) => {
8513                let mut accum = s;
8514                while matches!(self.peek(), Token::Dot) {
8515                    self.advance();
8516                    let next = self.expect_ident_like()?;
8517                    accum.push('.');
8518                    accum.push_str(&next);
8519                }
8520                Ok(crate::ast::SetValue::Ident(accum))
8521            }
8522            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8523            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8524            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8525            // spellings that lex as keyword tokens, not idents:
8526            // `SET standard_conforming_strings = on` is in every
8527            // pg_dump preamble (`off` already lexes as an ident).
8528            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8529            // DEFAULT lexes as its keyword token, so the ident arm above
8530            // never saw it and the everyday reset form was a syntax error.
8531            Token::Default => Ok(crate::ast::SetValue::Default),
8532            // v7.40.11 — MySQL 9.7.2 accepts `NULL` here for exactly one
8533            // variable and rejects it with error 1231 for every other
8534            // (both measured); PostgreSQL 18.6 rejects the token itself
8535            // with `syntax error at or near "NULL"`. So the token is
8536            // admitted only for a MySQL session and the per-variable
8537            // decision is the executor's.
8538            Token::Null if self.mysql_dialect => Ok(crate::ast::SetValue::Null),
8539            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8540            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8541            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8542            // v7.14.0 — MySQL session/user variable RHS
8543            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8544            // Wrap as Ident so the SET handler can record it; the
8545            // engine treats `@VAR` / `@@VAR` values as opaque
8546            // strings.
8547            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8548            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8549            // is the common MySQL preamble shape. Allow a `+` or
8550            // `-` prefix on negative numerics for parity with PG
8551            // (some param defaults are negative).
8552            Token::Minus => match self.advance() {
8553                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8554                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8555                other => Err(self.err(format!(
8556                    "expected numeric after `-` in SET value, got {other:?}"
8557                ))),
8558            },
8559            other => Err(self.err(format!(
8560                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8561            ))),
8562        }
8563    }
8564
8565    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8566    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8567    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8568    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8569    /// present). Modes are comma-separated per PG; SPG also
8570    /// accepts space-separated for tolerance. READ ONLY / WRITE
8571    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8572    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8573    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8574    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8575    /// session default rather than forcing READ COMMITTED.
8576    fn parse_isolation_level_clauses(
8577        &mut self,
8578    ) -> Result<crate::ast::TransactionModes, ParseError> {
8579        let mut level = IsolationLevel::default();
8580        let mut have_level = false;
8581        // v7.39 — READ ONLY / READ WRITE used to be consumed and dropped,
8582        // so `BEGIN READ ONLY` opened an ordinary read-write transaction.
8583        let mut read_only: Option<bool> = None;
8584        // v7.40.12 — DEFERRABLE was consumed and dropped, the way READ
8585        // ONLY was before v7.39. Measured on PG 18.6: `BEGIN DEFERRABLE;
8586        // SHOW transaction_deferrable` -> on; SPG answered off.
8587        let mut deferrable: Option<bool> = None;
8588        loop {
8589            // ISOLATION LEVEL …
8590            let saw_isolation =
8591                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8592            if saw_isolation {
8593                self.advance(); // ISOLATION
8594                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8595                    return Err(self.err(alloc::format!(
8596                        "expected LEVEL after ISOLATION, got {:?}",
8597                        self.peek()
8598                    )));
8599                }
8600                self.advance(); // LEVEL
8601                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8602                let w1 = self
8603                    .expect_ident_like()
8604                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8605                let lc = w1.to_ascii_lowercase();
8606                level = match lc.as_str() {
8607                    "serializable" => IsolationLevel::Serializable,
8608                    "repeatable" => {
8609                        // Expect READ
8610                        let w2 = self
8611                            .expect_ident_like()
8612                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8613                        if !w2.eq_ignore_ascii_case("read") {
8614                            return Err(self.err(alloc::format!(
8615                                "expected READ after REPEATABLE, got {w2:?}"
8616                            )));
8617                        }
8618                        IsolationLevel::RepeatableRead
8619                    }
8620                    "read" => {
8621                        let w2 = self
8622                            .expect_ident_like()
8623                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8624                        match w2.to_ascii_lowercase().as_str() {
8625                            "committed" => IsolationLevel::ReadCommitted,
8626                            "uncommitted" => IsolationLevel::ReadUncommitted,
8627                            other => {
8628                                return Err(self.err(alloc::format!(
8629                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8630                                )));
8631                            }
8632                        }
8633                    }
8634                    other => {
8635                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8636                    }
8637                };
8638                have_level = true;
8639            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8640                // v7.39 — READ ONLY | READ WRITE. The comment here used to
8641                // read "parsed, not behaviorally honoured", and it was
8642                // accurate: the clause was thrown away, so `BEGIN READ ONLY`
8643                // opened an ordinary read-write transaction and accepted
8644                // every write in it.
8645                self.advance();
8646                match self.peek().clone() {
8647                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8648                        self.advance();
8649                        read_only = Some(true);
8650                    }
8651                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8652                        self.advance();
8653                        read_only = Some(false);
8654                    }
8655                    other => {
8656                        return Err(self.err(alloc::format!(
8657                            "expected ONLY or WRITE after READ, got {other:?}"
8658                        )));
8659                    }
8660                }
8661            } else if matches!(self.peek(), Token::Not) {
8662                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8663                self.advance();
8664                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8665                    return Err(self.err(alloc::format!(
8666                        "expected DEFERRABLE after NOT, got {:?}",
8667                        self.peek()
8668                    )));
8669                }
8670                self.advance();
8671                deferrable = Some(false);
8672            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8673            {
8674                self.advance();
8675                deferrable = Some(true);
8676            } else {
8677                break;
8678            }
8679            // Optional comma between modes.
8680            if matches!(self.peek(), Token::Comma) {
8681                self.advance();
8682            }
8683        }
8684        Ok(crate::ast::TransactionModes {
8685            isolation: have_level.then_some(level),
8686            read_only,
8687            deferrable,
8688        })
8689    }
8690
8691    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8692        // FOR is a v6.1.2-reserved keyword (Token::For). The
8693        // other two are bare idents — they've never needed lexer
8694        // support and we keep it that way.
8695        if !matches!(self.peek(), Token::For) {
8696            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8697        }
8698        self.advance();
8699        self.expect_keyword_ident("wal")?;
8700        self.expect_keyword_ident("position")?;
8701        let pos = self.expect_u64_literal()?;
8702        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8703        {
8704            self.advance();
8705            self.expect_keyword_ident("timeout")?;
8706            Some(self.expect_u64_literal()?)
8707        } else {
8708            None
8709        };
8710        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8711    }
8712
8713    /// v6.1.7 helper — consume a `Token::Integer` and check it
8714    /// fits `u64`. WAL positions and millisecond timeouts are
8715    /// non-negative.
8716    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8717        match self.advance() {
8718            Token::Integer(n) if n >= 0 => Ok(n as u64),
8719            Token::Integer(n) => Err(ParseError {
8720                message: format!("expected non-negative integer, got {n}"),
8721                token_pos: self.consumed_pos(),
8722            }),
8723            other => Err(ParseError {
8724                message: format!("expected integer literal, got {other:?}"),
8725                token_pos: self.consumed_pos(),
8726            }),
8727        }
8728    }
8729
8730    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8731    /// ROLE '<role>' (defaults to readonly). All string slots accept
8732    /// either a quoted ident or a quoted string literal.
8733    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8734    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8735    ///
8736    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8737    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8738    /// wire role) still parses — it is a different axis from the PG attributes.
8739    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8740    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8741    /// or RESET, so the plain attribute forms keep their old path.
8742    fn peeks_db_role_setting(&self) -> bool {
8743        let mut i = self.pos + 1; // past the object's name
8744        let word = |p: usize| -> Option<String> {
8745            match self.tokens.get(p) {
8746                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8747                Some(Token::In) => Some(String::from("in")),
8748                _ => None,
8749            }
8750        };
8751        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8752            i += 3; // IN DATABASE <name>
8753        }
8754        matches!(word(i).as_deref(), Some("set" | "reset"))
8755    }
8756
8757    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8758        use crate::ast::SetDbRoleSettingStatement;
8759        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8760        // identifier, so the ordinary name reader refuses it. Same trap
8761        // as TABLE / INDEX / FULL / DEFAULT before it.
8762        let name = if matches!(self.peek(), Token::All) {
8763            self.advance();
8764            String::from("all")
8765        } else {
8766            self.expect_ident_or_string()?
8767        };
8768        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8769        let all = name.eq_ignore_ascii_case("all");
8770        let (mut database, mut role) = if is_database {
8771            (Some(name), None)
8772        } else if all {
8773            (None, None)
8774        } else {
8775            (None, Some(name))
8776        };
8777        if matches!(self.peek(), Token::In) {
8778            self.advance();
8779            self.advance(); // DATABASE
8780            database = Some(self.expect_ident_or_string()?);
8781        }
8782        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8783        self.advance(); // SET | RESET
8784        if resetting && matches!(self.peek(), Token::All) {
8785            self.advance();
8786            self.consume_until_statement_boundary();
8787            return Ok(Statement::SetDbRoleSetting(Box::new(
8788                SetDbRoleSettingStatement {
8789                    database,
8790                    role,
8791                    param: None,
8792                    value: None,
8793                },
8794            )));
8795        }
8796        let param = self.expect_ident_like()?;
8797        let value = if resetting {
8798            None
8799        } else {
8800            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8801            // KEYWORD, so the ident-only check missed it and consumed
8802            // the word itself as the value — the same trap as ALL, one
8803            // clause over.
8804            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8805                self.advance();
8806            }
8807            Some(self.take_guc_value())
8808        };
8809        self.consume_until_statement_boundary();
8810        Ok(Statement::SetDbRoleSetting(Box::new(
8811            SetDbRoleSettingStatement {
8812                database,
8813                role,
8814                param: Some(param),
8815                value,
8816            },
8817        )))
8818    }
8819
8820    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8821    /// a quoted literal loses its quotes, a bare word or number does not.
8822    fn take_guc_value(&mut self) -> String {
8823        match self.advance() {
8824            Token::String(s) => s,
8825            Token::Integer(n) => format!("{n}"),
8826            Token::Float(f) => format!("{f}"),
8827            Token::Ident(s) | Token::QuotedIdent(s) => s,
8828            other => format!("{other:?}"),
8829        }
8830    }
8831
8832    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8833        let name = self.expect_ident_or_string()?;
8834        if self.peek_keyword_ident("with") {
8835            self.advance();
8836        }
8837        let mut password = String::new();
8838        let mut role = String::new();
8839        let mut login: Option<bool> = None;
8840        let mut inherit: Option<bool> = None;
8841        let mut superuser: Option<bool> = None;
8842        // Not a `while let`: the pattern would borrow `self` across the
8843        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8844        #[allow(clippy::while_let_loop)]
8845        loop {
8846            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8847                break;
8848            };
8849            match w.to_ascii_lowercase().as_str() {
8850                "password" => {
8851                    self.advance();
8852                    password = self.expect_string_literal()?;
8853                }
8854                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8855                // is the same slot.
8856                "encrypted" => {
8857                    self.advance();
8858                    self.expect_keyword_ident("password")?;
8859                    password = self.expect_string_literal()?;
8860                }
8861                "login" => {
8862                    self.advance();
8863                    login = Some(true);
8864                }
8865                "nologin" => {
8866                    self.advance();
8867                    login = Some(false);
8868                }
8869                "inherit" => {
8870                    self.advance();
8871                    inherit = Some(true);
8872                }
8873                "noinherit" => {
8874                    self.advance();
8875                    inherit = Some(false);
8876                }
8877                "superuser" => {
8878                    self.advance();
8879                    superuser = Some(true);
8880                }
8881                "nosuperuser" => {
8882                    self.advance();
8883                    superuser = Some(false);
8884                }
8885                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8886                "role" => {
8887                    self.advance();
8888                    role = self.expect_string_literal()?;
8889                }
8890                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8891                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8892                // accepted and ignored so a pg_dump role block restores. They
8893                // gate capabilities SPG does not have.
8894                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8895                | "noreplication" | "bypassrls" | "nobypassrls" => {
8896                    self.advance();
8897                }
8898                "connection" => {
8899                    self.advance();
8900                    self.expect_keyword_ident("limit")?;
8901                    self.advance(); // the number
8902                }
8903                "valid" => {
8904                    self.advance();
8905                    self.expect_keyword_ident("until")?;
8906                    self.expect_string_literal()?;
8907                }
8908                _ => break,
8909            }
8910        }
8911        if role.is_empty() {
8912            role = "readonly".to_string();
8913        }
8914        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8915            name,
8916            password,
8917            role,
8918            login,
8919            inherit,
8920            superuser,
8921            is_user,
8922        }))
8923    }
8924
8925    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8926    /// consumed the USING / WITH CHECK keyword.
8927    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8928        if !matches!(self.peek(), Token::LParen) {
8929            return Err(self.err(alloc::format!(
8930                "expected '(' after {clause}, got {:?}",
8931                self.peek()
8932            )));
8933        }
8934        self.advance();
8935        let e = self.parse_expr(0)?;
8936        if !matches!(self.peek(), Token::RParen) {
8937            return Err(self.err(alloc::format!(
8938                "expected ')' to close {clause}, got {:?}",
8939                self.peek()
8940            )));
8941        }
8942        self.advance();
8943        Ok(e)
8944    }
8945
8946    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8947    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8948        let mut roles = Vec::new();
8949        loop {
8950            roles.push(self.expect_ident_like()?);
8951            if matches!(self.peek(), Token::Comma) {
8952                self.advance();
8953            } else {
8954                break;
8955            }
8956        }
8957        Ok(roles)
8958    }
8959
8960    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8961    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8962    /// `CREATE POLICY`.
8963    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8964        use crate::ast::PolicyCmd;
8965        let name = self.expect_ident_like()?;
8966        if !matches!(self.peek(), Token::On) {
8967            return Err(self.err(alloc::format!(
8968                "expected ON after CREATE POLICY name, got {:?}",
8969                self.peek()
8970            )));
8971        }
8972        self.advance();
8973        let table = self.expect_ident_like()?;
8974
8975        let mut permissive = true;
8976        if matches!(self.peek(), Token::As) {
8977            self.advance();
8978            let w = self.expect_ident_like()?;
8979            permissive = if w.eq_ignore_ascii_case("permissive") {
8980                true
8981            } else if w.eq_ignore_ascii_case("restrictive") {
8982                false
8983            } else {
8984                return Err(self.err(alloc::format!(
8985                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8986                )));
8987            };
8988        }
8989
8990        let mut cmd = PolicyCmd::All;
8991        if matches!(self.peek(), Token::For) {
8992            self.advance();
8993            cmd = self.parse_policy_cmd()?;
8994        }
8995
8996        let mut roles = Vec::new();
8997        if matches!(self.peek(), Token::To) {
8998            self.advance();
8999            roles = self.parse_policy_roles()?;
9000        }
9001
9002        let mut using = None;
9003        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
9004        {
9005            self.advance();
9006            using = Some(self.parse_paren_expr("USING")?);
9007        }
9008
9009        let mut with_check = None;
9010        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
9011        {
9012            self.advance();
9013            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
9014            {
9015                return Err(self.err(alloc::format!(
9016                    "expected CHECK after WITH, got {:?}",
9017                    self.peek()
9018                )));
9019            }
9020            self.advance();
9021            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
9022        }
9023
9024        // Clause-per-command matrix (PG wording).
9025        match cmd {
9026            PolicyCmd::Insert => {
9027                if using.is_some() {
9028                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
9029                }
9030            }
9031            PolicyCmd::Select | PolicyCmd::Delete => {
9032                if with_check.is_some() {
9033                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
9034                }
9035            }
9036            PolicyCmd::Update | PolicyCmd::All => {}
9037        }
9038
9039        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
9040            name,
9041            table,
9042            permissive,
9043            cmd,
9044            roles,
9045            using,
9046            with_check,
9047        }))
9048    }
9049
9050    /// v7.39 (RLS) — the command word after `FOR`.
9051    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
9052        use crate::ast::PolicyCmd;
9053        match self.peek().clone() {
9054            Token::All => {
9055                self.advance();
9056                Ok(PolicyCmd::All)
9057            }
9058            Token::Select => {
9059                self.advance();
9060                Ok(PolicyCmd::Select)
9061            }
9062            Token::Insert => {
9063                self.advance();
9064                Ok(PolicyCmd::Insert)
9065            }
9066            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
9067                self.advance();
9068                Ok(PolicyCmd::Update)
9069            }
9070            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
9071                self.advance();
9072                Ok(PolicyCmd::Delete)
9073            }
9074            other => Err(self.err(alloc::format!(
9075                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
9076            ))),
9077        }
9078    }
9079
9080    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
9081    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
9082    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9083        let name = self.expect_ident_like()?;
9084        if !matches!(self.peek(), Token::On) {
9085            return Err(self.err(alloc::format!(
9086                "expected ON after ALTER POLICY name, got {:?}",
9087                self.peek()
9088            )));
9089        }
9090        self.advance();
9091        let table = self.expect_ident_like()?;
9092
9093        // RENAME TO new
9094        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
9095        {
9096            self.advance();
9097            if !matches!(self.peek(), Token::To) {
9098                return Err(self.err(alloc::format!(
9099                    "expected TO after RENAME, got {:?}",
9100                    self.peek()
9101                )));
9102            }
9103            self.advance();
9104            let new = self.expect_ident_like()?;
9105            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9106                name,
9107                table,
9108                rename_to: Some(new),
9109                roles: None,
9110                using: None,
9111                with_check: None,
9112            }));
9113        }
9114
9115        let mut roles = None;
9116        if matches!(self.peek(), Token::To) {
9117            self.advance();
9118            roles = Some(self.parse_policy_roles()?);
9119        }
9120        let mut using = None;
9121        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
9122        {
9123            self.advance();
9124            using = Some(self.parse_paren_expr("USING")?);
9125        }
9126        let mut with_check = None;
9127        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
9128        {
9129            self.advance();
9130            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
9131            {
9132                return Err(self.err(alloc::format!(
9133                    "expected CHECK after WITH, got {:?}",
9134                    self.peek()
9135                )));
9136            }
9137            self.advance();
9138            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
9139        }
9140        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9141            name,
9142            table,
9143            rename_to: None,
9144            roles,
9145            using,
9146            with_check,
9147        }))
9148    }
9149
9150    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
9151    /// `DROP POLICY`.
9152    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9153        let if_exists = self.consume_if_exists();
9154        let name = self.expect_ident_like()?;
9155        if !matches!(self.peek(), Token::On) {
9156            return Err(self.err(alloc::format!(
9157                "expected ON after DROP POLICY name, got {:?}",
9158                self.peek()
9159            )));
9160        }
9161        self.advance();
9162        let table = self.expect_ident_like()?;
9163        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
9164            name,
9165            table,
9166            if_exists,
9167        }))
9168    }
9169}
9170fn wrap_from_leaves(
9171    e: &mut Expr,
9172    names: &[String],
9173    make: &dyn Fn(Expr) -> Expr,
9174    refs: &dyn Fn(&Expr) -> bool,
9175) {
9176    if let Expr::Column(c) = e {
9177        if c.qualifier
9178            .as_deref()
9179            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
9180        {
9181            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
9182            *e = make(taken);
9183        }
9184        return;
9185    }
9186    match e {
9187        Expr::Binary { lhs, rhs, .. } => {
9188            wrap_from_leaves(lhs, names, make, refs);
9189            wrap_from_leaves(rhs, names, make, refs);
9190        }
9191        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
9192            wrap_from_leaves(expr, names, make, refs)
9193        }
9194        Expr::FunctionCall { args, .. } => {
9195            for a in args.iter_mut() {
9196                wrap_from_leaves(a, names, make, refs);
9197            }
9198        }
9199        Expr::Case {
9200            operand,
9201            branches,
9202            else_branch,
9203        } => {
9204            if let Some(o) = operand.as_deref_mut() {
9205                wrap_from_leaves(o, names, make, refs);
9206            }
9207            for (w, t) in branches.iter_mut() {
9208                wrap_from_leaves(w, names, make, refs);
9209                wrap_from_leaves(t, names, make, refs);
9210            }
9211            if let Some(el) = else_branch.as_deref_mut() {
9212                wrap_from_leaves(el, names, make, refs);
9213            }
9214        }
9215        // Compound variants the walk doesn't decompose: keep the
9216        // pre-D.30 behavior — wrap the whole sub-expr if it touches
9217        // a source table, so nothing regresses.
9218        other => {
9219            if refs(other) {
9220                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
9221                *other = make(taken);
9222            }
9223        }
9224    }
9225}
9226
9227/// v7.39 (round 241) — does this expression reference any of the FROM /
9228/// USING table names (shared by the UPDATE…FROM and DELETE…USING
9229/// lowerings)?
9230fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
9231    match e {
9232        Expr::Column(c) => c
9233            .qualifier
9234            .as_deref()
9235            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9236        Expr::Binary { lhs, rhs, .. } => {
9237            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
9238        }
9239        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
9240        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
9241        Expr::Case {
9242            operand,
9243            branches,
9244            else_branch,
9245        } => {
9246            operand
9247                .as_deref()
9248                .is_some_and(|o| expr_refs_tables(o, names))
9249                || branches
9250                    .iter()
9251                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
9252                || else_branch
9253                    .as_deref()
9254                    .is_some_and(|el| expr_refs_tables(el, names))
9255        }
9256        _ => false,
9257    }
9258}
9259
9260impl Parser {
9261    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
9262    /// Caller already consumed the leading `UPDATE` ident.
9263    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
9264    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
9265    /// after the target name has been read. `JOIN` is a reserved token;
9266    /// the qualifiers are bare idents.
9267    fn peek_is_update_join_start(&self) -> bool {
9268        match self.peek() {
9269            // JOIN and its qualifiers are reserved lexer tokens (the grammar
9270            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
9271            Token::Join
9272            | Token::Inner
9273            | Token::Left
9274            | Token::Right
9275            | Token::Cross
9276            | Token::Full => true,
9277            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
9278            Token::Ident(s) | Token::QuotedIdent(s) => {
9279                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
9280            }
9281            _ => false,
9282        }
9283    }
9284
9285    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
9286    /// USER-variable assignment. Its own per-session namespace, an arbitrary
9287    /// expression on the right, and `:=` as a second spelling of `=`.
9288    ///
9289    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
9290    /// from sits on the nesting recursion chain (a CTE body, a subquery),
9291    /// and holding this loop's `Vec` + `String` locals there overflowed the
9292    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
9293    #[inline(never)]
9294    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
9295        let mut assigns: Vec<(String, Expr)> = Vec::new();
9296        let mut settings: Vec<(String, Expr)> = Vec::new();
9297        loop {
9298            // v7.39 (round 554) — a plain NAME here is a session
9299            // setting, not a user variable. mysqldump writes the two in
9300            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
9301            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
9302            // changes it — and this refused the mixture outright, so no
9303            // dump could be restored past its preamble.
9304            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9305                self.advance();
9306                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9307                    return Err(self.err(alloc::format!(
9308                        "expected `=` after {name}, got {:?}",
9309                        self.peek()
9310                    )));
9311                }
9312                self.advance();
9313                let value = self.parse_expr(0)?;
9314                settings.push((name.to_ascii_lowercase(), value));
9315                if matches!(self.peek(), Token::Comma) {
9316                    self.advance();
9317                    continue;
9318                }
9319                break;
9320            }
9321            let Token::SessionVar(raw) = self.peek().clone() else {
9322                return Err(self.err(alloc::format!(
9323                    "expected a user variable after SET, got {:?}",
9324                    self.peek()
9325                )));
9326            };
9327            if raw.starts_with("@@") {
9328                return Err(self.err(alloc::string::String::from(
9329                    "cannot mix `@@` settings with `@` user variables in one SET",
9330                )));
9331            }
9332            self.advance();
9333            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9334                return Err(self.err(alloc::format!(
9335                    "expected `=` or `:=` after {raw}, got {:?}",
9336                    self.peek()
9337                )));
9338            }
9339            self.advance();
9340            let value = self.parse_expr(0)?;
9341            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9342            if matches!(self.peek(), Token::Comma) {
9343                self.advance();
9344                continue;
9345            }
9346            break;
9347        }
9348        Ok(Statement::SetUserVars(assigns, settings))
9349    }
9350
9351    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9352        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9353        // NAMED `only` until now, which failed on `relation "only" does
9354        // not exist`. The lookahead is what keeps a table actually
9355        // called `only` working: the keyword is only a keyword when a
9356        // TABLE NAME follows it — and `SET` arrives as an identifier
9357        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9358        // for the table and die on the `=`. Measured by the pin.
9359        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9360            if s.eq_ignore_ascii_case("only"))
9361            && matches!(
9362                self.tokens.get(self.pos + 1),
9363                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9364            );
9365        if only {
9366            self.advance();
9367        }
9368        let table = self.expect_ident_like()?;
9369        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9370        // bare spelling; a bare identifier that is the SET keyword itself
9371        // is the clause, not an alias.
9372        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9373        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9374        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9375        // following JOIN a syntax error.
9376        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9377        let alias = if matches!(self.peek(), Token::As) {
9378            self.advance();
9379            Some(self.expect_ident_like()?)
9380        } else {
9381            match self.peek() {
9382                Token::Ident(s) | Token::QuotedIdent(s)
9383                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
9384                {
9385                    let a = s.clone();
9386                    self.advance();
9387                    Some(a)
9388                }
9389                _ => None,
9390            }
9391        };
9392        // v7.39 (round 420) — MySQL's multi-table UPDATE:
9393        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
9394        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
9395        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9396        // The FIRST table is the mutation target and the rest are sources —
9397        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9398        // SPG already lowers onto correlated subqueries. So rewind, let
9399        // `parse_from_clause` read the whole list (it handles aliases, comma
9400        // lists, and every JOIN form), then peel the target off the front.
9401        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9402            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9403        {
9404            // NOTE: `advance()` destroys the tokens it returns
9405            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9406            // is NOT possible — the tail is read forward, once, through the
9407            // same grammar `parse_from_clause` uses after its primary.
9408            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9409            let mut joins = self.parse_from_joins(&target_qual)?;
9410            if joins.is_empty() {
9411                return Err(self.err(alloc::string::String::from(
9412                    "multi-table UPDATE needs at least one source table",
9413                )));
9414            }
9415            let head = joins.remove(0);
9416            // A LEFT join keeps every target row (the unmatched ones see NULL
9417            // on the source side), so it must NOT get the EXISTS row filter
9418            // the inner / comma forms use.
9419            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9420            let src = FromClause {
9421                primary: head.table,
9422                joins,
9423            };
9424            (Some(src), head.on, outer)
9425        } else {
9426            (None, None, false)
9427        };
9428        self.expect_keyword_ident("set")?;
9429        let mut assignments = Vec::new();
9430        loop {
9431            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9432            // …)` — the parenthesized multi-assignment. Expressions
9433            // assign positionally; a subquery RHS clones per column
9434            // keeping only the Nth projection item.
9435            if matches!(self.peek(), Token::LParen) {
9436                self.advance();
9437                let mut cols = alloc::vec![self.expect_ident_like()?];
9438                while matches!(self.peek(), Token::Comma) {
9439                    self.advance();
9440                    cols.push(self.expect_ident_like()?);
9441                }
9442                if !matches!(self.peek(), Token::RParen) {
9443                    return Err(self.err(format!(
9444                        "expected ')' after SET column list, got {:?}",
9445                        self.peek()
9446                    )));
9447                }
9448                self.advance();
9449                if !matches!(self.peek(), Token::Eq) {
9450                    return Err(self.err(format!(
9451                        "expected `=` after SET column list, got {:?}",
9452                        self.peek()
9453                    )));
9454                }
9455                self.advance();
9456                if !matches!(self.peek(), Token::LParen) {
9457                    return Err(self.err(format!(
9458                        "expected '(' after SET (…) =, got {:?}",
9459                        self.peek()
9460                    )));
9461                }
9462                self.advance();
9463                if matches!(self.peek(), Token::Select) {
9464                    let inner = match self.parse_select_stmt()? {
9465                        Statement::Select(s) => s,
9466                        other => {
9467                            return Err(self.err(alloc::format!(
9468                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9469                            )));
9470                        }
9471                    };
9472                    if !matches!(self.peek(), Token::RParen) {
9473                        return Err(self.err(format!(
9474                            "expected ')' after SET subquery, got {:?}",
9475                            self.peek()
9476                        )));
9477                    }
9478                    self.advance();
9479                    if inner.items.len() != cols.len() {
9480                        return Err(self.err(alloc::format!(
9481                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9482                            cols.len(),
9483                            inner.items.len()
9484                        )));
9485                    }
9486                    for (i, col) in cols.into_iter().enumerate() {
9487                        let mut sub = inner.clone();
9488                        sub.items = alloc::vec![sub.items[i].clone()];
9489                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9490                    }
9491                } else {
9492                    let mut exprs = alloc::vec![self.parse_expr(0)?];
9493                    while matches!(self.peek(), Token::Comma) {
9494                        self.advance();
9495                        exprs.push(self.parse_expr(0)?);
9496                    }
9497                    if !matches!(self.peek(), Token::RParen) {
9498                        return Err(self.err(format!(
9499                            "expected ')' after SET row values, got {:?}",
9500                            self.peek()
9501                        )));
9502                    }
9503                    self.advance();
9504                    if exprs.len() != cols.len() {
9505                        return Err(self.err(alloc::format!(
9506                            "SET (…) = (…) arity mismatch: {} columns, {} values",
9507                            cols.len(),
9508                            exprs.len()
9509                        )));
9510                    }
9511                    for (col, e) in cols.into_iter().zip(exprs) {
9512                        assignments.push((col, e));
9513                    }
9514                }
9515                if matches!(self.peek(), Token::Comma) {
9516                    self.advance();
9517                    continue;
9518                }
9519                break;
9520            }
9521            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9522            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9523            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9524            // `public.` dump qualifiers), so the qualifier has to be read off
9525            // the token stream first — otherwise `SET b.v = 888` would write
9526            // to the TARGET table's `v` while naming a source table, a
9527            // silent-wrong. A qualifier naming a SOURCE table means a
9528            // multi-TARGET update — mutating two tables in one statement —
9529            // which SPG does not model, so it is refused loudly.
9530            let set_qual: Option<String> = if mysql_from.is_some()
9531                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9532            {
9533                match self.peek() {
9534                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9535                    _ => None,
9536                }
9537            } else {
9538                None
9539            };
9540            let col = self.expect_ident_like()?;
9541            if let Some(q) = set_qual {
9542                let names_target = q.eq_ignore_ascii_case(&table)
9543                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9544                if !names_target {
9545                    return Err(self.err(alloc::format!(
9546                        "multi-table UPDATE can only assign to its first table \
9547                         ({table}); `{q}.{col}` targets another table"
9548                    )));
9549                }
9550            }
9551            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9552            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9553            // `__column_default` marker lowering just below). PG assigns to the
9554            // i-th (1-based) element, NULL-padding when i exceeds the length.
9555            if matches!(self.peek(), Token::LBracket) {
9556                self.advance();
9557                let index = self.parse_expr(0)?;
9558                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9559                // (and the open `arr[lo:]`), lowered to
9560                // `__array_assign_slice`. Only the single-subscript form
9561                // parsed before, so a slice assignment was a syntax error.
9562                let mut slice_hi: Option<Option<Expr>> = None;
9563                if matches!(self.peek(), Token::Colon) {
9564                    self.advance();
9565                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9566                        None
9567                    } else {
9568                        Some(self.parse_expr(0)?)
9569                    });
9570                }
9571                if !matches!(self.peek(), Token::RBracket) {
9572                    return Err(self.err(format!(
9573                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9574                        self.peek()
9575                    )));
9576                }
9577                self.advance();
9578                if !matches!(self.peek(), Token::Eq) {
9579                    return Err(self.err(format!(
9580                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9581                        self.peek()
9582                    )));
9583                }
9584                self.advance();
9585                let value = self.parse_expr(0)?;
9586                // PG merges several subscript writes to the same column into one
9587                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9588                // assignment to `col` rather than each overwriting the original.
9589                let existing = assignments.iter().position(|(c, _)| c == &col);
9590                let base = match existing {
9591                    Some(i) => assignments[i].1.clone(),
9592                    None => Expr::Column(ColumnName {
9593                        qualifier: None,
9594                        name: col.clone(),
9595                    }),
9596                };
9597                let assigned = match slice_hi {
9598                    None => Expr::FunctionCall {
9599                        name: "__array_assign".to_string(),
9600                        args: alloc::vec![base, index, value],
9601                    },
9602                    Some(hi) => Expr::FunctionCall {
9603                        name: "__array_assign_slice".to_string(),
9604                        args: alloc::vec![
9605                            base,
9606                            index,
9607                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9608                            value,
9609                        ],
9610                    },
9611                };
9612                match existing {
9613                    Some(i) => assignments[i].1 = assigned,
9614                    None => assignments.push((col, assigned)),
9615                }
9616                if matches!(self.peek(), Token::Comma) {
9617                    self.advance();
9618                    continue;
9619                }
9620                break;
9621            }
9622            if !matches!(self.peek(), Token::Eq) {
9623                return Err(self.err(format!(
9624                    "expected `=` after column name in UPDATE SET, got {:?}",
9625                    self.peek()
9626                )));
9627            }
9628            self.advance();
9629            // `SET col = DEFAULT` — the column's declared default;
9630            // rides out as a marker call the update executor
9631            // resolves against the schema.
9632            let value = if matches!(self.peek(), Token::Default) {
9633                self.advance();
9634                Expr::FunctionCall {
9635                    name: "__column_default".to_string(),
9636                    args: Vec::new(),
9637                }
9638            } else {
9639                self.parse_expr(0)?
9640            };
9641            assignments.push((col, value));
9642            if matches!(self.peek(), Token::Comma) {
9643                self.advance();
9644                continue;
9645            }
9646            break;
9647        }
9648        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9649        // update. Lowers onto the correlated-subquery machinery:
9650        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9651        // and each assignment that references a FROM-list table
9652        // wraps into a correlated scalar subquery
9653        // (SELECT expr FROM src WHERE cond). Equivalent for the
9654        // unique-join shape (the overwhelmingly common one); a
9655        // multi-match, which PG resolves by arbitrary pick,
9656        // surfaces as a scalar-subquery cardinality error instead
9657        // of a silent arbitrary result.
9658        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9659        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9660        // the SAME lowering below. Both spellings together is not legal in
9661        // either dialect.
9662        let from_clause = if let Some(fc) = mysql_from {
9663            if matches!(self.peek(), Token::From) {
9664                return Err(self.err(alloc::string::String::from(
9665                    "multi-table UPDATE already names its sources; drop the FROM clause",
9666                )));
9667            }
9668            Some(fc)
9669        } else if matches!(self.peek(), Token::From) {
9670            self.advance();
9671            Some(self.parse_from_clause()?)
9672        } else {
9673            None
9674        };
9675        let where_ = if matches!(self.peek(), Token::Where) {
9676            self.advance();
9677            Some(self.parse_expr(0)?)
9678        } else {
9679            None
9680        };
9681        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9682        // and the TARGET-row filter are NOT the same predicate once a LEFT
9683        // join is involved:
9684        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9685        //     one conjunction, and the whole thing filters target rows via
9686        //     EXISTS.
9687        //   * LEFT join: only the ON predicate belongs inside the source
9688        //     subquery. The WHERE still filters TARGET rows (with source
9689        //     columns read through the correlated subquery, which yields NULL
9690        //     for an unmatched row — exactly LEFT-join semantics).
9691        // Round 420 folded ON into WHERE unconditionally and then dropped the
9692        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9693        // WHERE a.id > 1` updated EVERY row.
9694        let sub_where = match (mysql_on.clone(), where_.clone()) {
9695            _ if mysql_outer => mysql_on.clone(),
9696            (Some(on), Some(w)) => Some(Expr::Binary {
9697                lhs: Box::new(on),
9698                op: crate::ast::BinOp::And,
9699                rhs: Box::new(w),
9700            }),
9701            (Some(on), None) => Some(on),
9702            (None, w) => w,
9703        };
9704        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9705        // has no such clause on UPDATE, so this is accepted only under the
9706        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9707        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9708        let mut returning = self.parse_optional_returning()?;
9709        // v7.39 (round 533) — kept for the engine, which can resolve the
9710        // UNQUALIFIED leaves this lowering has to leave alone.
9711        let from_sources = from_clause.as_ref().map(|fc| {
9712            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9713                from: fc.clone(),
9714                sub_where: sub_where.clone(),
9715            })
9716        });
9717        let (assignments, where_) = if let Some(fc) = from_clause {
9718            let names: Vec<String> = core::iter::once(&fc.primary)
9719                .chain(fc.joins.iter().map(|j| &j.table))
9720                .flat_map(|t| {
9721                    t.alias
9722                        .clone()
9723                        .into_iter()
9724                        .chain(core::iter::once(t.name.clone()))
9725                })
9726                .collect();
9727            let refs_list = |e: &Expr| -> bool {
9728                fn walk(e: &Expr, names: &[String]) -> bool {
9729                    match e {
9730                        Expr::Column(c) => c
9731                            .qualifier
9732                            .as_deref()
9733                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9734                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9735                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9736                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9737                        Expr::Case {
9738                            operand,
9739                            branches,
9740                            else_branch,
9741                        } => {
9742                            operand.as_deref().is_some_and(|o| walk(o, names))
9743                                || branches
9744                                    .iter()
9745                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9746                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9747                        }
9748                        _ => false,
9749                    }
9750                }
9751                walk(e, &names)
9752            };
9753            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9754                locking: None,
9755                ctes: Vec::new(),
9756                distinct: false,
9757                distinct_on: Vec::new(),
9758                items,
9759                from: Some(fc.clone()),
9760                where_: sub_where.clone(),
9761                group_by: None,
9762                group_by_all: false,
9763                having: None,
9764                unions: Vec::new(),
9765                order_by: Vec::new(),
9766                limit: None,
9767                offset: None,
9768                limit_with_ties: false,
9769                window_check_exprs: Vec::new(),
9770            };
9771            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9772            // assignment RHS with a correlated scalar subquery, instead of
9773            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9774            // column reference (`SET v = v + u.bonus`, where `v` is the target
9775            // table's column) inside a subquery whose FROM only has the source
9776            // table, so the unqualified `v` resolved against the source and
9777            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9778            // context — where they belong — fixes it; only the source columns
9779            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9780            // compound variants the leaf-walk doesn't decompose.
9781            let make_subq = |inner: Expr| {
9782                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9783                    expr: inner,
9784                    alias: None,
9785                }])))
9786            };
9787            let assignments = assignments
9788                .into_iter()
9789                .map(|(col, mut expr)| {
9790                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9791                    (col, expr)
9792                })
9793                .collect();
9794            let exists = Expr::Exists {
9795                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9796                    expr: Expr::Literal(Literal::Integer(1)),
9797                    alias: None,
9798                }])),
9799                negated: false,
9800            };
9801            // v7.39 (round 241) — RETURNING may reference the FROM-list
9802            // tables too (`RETURNING emp.id, dept.name`); the same
9803            // leaf-to-correlated-subquery lowering the assignments get.
9804            // Without it the qualifier died at eval with "unknown table
9805            // qualifier". (RETURNING was parsed before this block — the
9806            // lowering is a pure AST transformation.)
9807            if let Some(items) = returning.as_mut() {
9808                for item in items.iter_mut() {
9809                    if let SelectItem::Expr { expr, .. } = item {
9810                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9811                    }
9812                }
9813            }
9814            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9815            // EVERY matching target row: it gets no EXISTS filter, but the
9816            // caller's WHERE still applies, with source columns read through
9817            // the correlated subquery (NULL when unmatched — LEFT-join
9818            // semantics). `sub_where` above already excluded the WHERE from
9819            // the source subquery for this case.
9820            if mysql_outer {
9821                let mut outer = where_;
9822                if let Some(w) = outer.as_mut() {
9823                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9824                }
9825                (assignments, outer)
9826            } else {
9827                (assignments, Some(exists))
9828            }
9829        } else {
9830            (assignments, where_)
9831        };
9832        Ok(Statement::Update(crate::ast::UpdateStatement {
9833            ctes: Vec::new(),
9834            table,
9835            only,
9836            alias,
9837            assignments,
9838            from_sources,
9839            where_,
9840            order_limit: update_order_limit,
9841            returning,
9842        }))
9843    }
9844
9845    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9846    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9847    /// clause and its meaning are identical, so both call this rather than
9848    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9849    /// legal. PG has no such clause on either statement, so it is read only
9850    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9851    /// errors.
9852    ///
9853    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9854    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9855    /// stack in round 430.
9856    #[inline(never)]
9857    fn parse_mysql_dml_order_limit(
9858        &mut self,
9859        what: &str,
9860    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9861        if !self.mysql_dialect {
9862            return Ok(None);
9863        }
9864        let order_by = self.parse_order_by_keys()?;
9865        let limit = if matches!(self.peek(), Token::Limit) {
9866            self.advance();
9867            let tok = self.advance();
9868            let Token::Integer(n) = tok else {
9869                return Err(self.err(alloc::format!(
9870                    "expected integer after {what} LIMIT, got {tok:?}"
9871                )));
9872            };
9873            // MySQL rejects the `LIMIT offset, count` form here — only a
9874            // single row count is legal on a DML statement.
9875            if matches!(self.peek(), Token::Comma) {
9876                return Err(self.err(alloc::format!(
9877                    "{what} LIMIT takes a row count, not an offset"
9878                )));
9879            }
9880            let n = u32::try_from(n)
9881                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9882            Some(n)
9883        } else {
9884            None
9885        };
9886        if order_by.is_empty() && limit.is_none() {
9887            return Ok(None);
9888        }
9889        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9890            order_by,
9891            limit,
9892        })))
9893    }
9894
9895    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9896    /// the leading `DELETE` ident.
9897    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9898        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9899        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9900        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9901        // parse here; it reaches the existing USING path with the target
9902        // repeated in the list, which the source-list peel below handles.)
9903        // More than one name is a multi-TARGET delete, which SPG does not
9904        // model; it is refused rather than half-applied.
9905        let mysql_pre_target: Option<String> =
9906            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9907                let first = self.expect_ident_like()?;
9908                if matches!(self.peek(), Token::Comma) {
9909                    return Err(self.err(alloc::format!(
9910                        "multi-table DELETE can only delete from one table; \
9911                     `DELETE {first}, …` names several"
9912                    )));
9913                }
9914                Some(first)
9915            } else {
9916                None
9917            };
9918        if !matches!(self.peek(), Token::From) {
9919            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9920        }
9921        self.advance();
9922        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9923        // lookahead as the UPDATE spelling.
9924        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9925            if s.eq_ignore_ascii_case("only"))
9926            && matches!(
9927                self.tokens.get(self.pos + 1),
9928                Some(Token::Ident(_) | Token::QuotedIdent(_))
9929            );
9930        if only {
9931            self.advance();
9932        }
9933        let table = self.expect_ident_like()?;
9934        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9935        // spelling must not swallow the clause keywords that can follow
9936        // the target.
9937        let alias = if matches!(self.peek(), Token::As) {
9938            self.advance();
9939            Some(self.expect_ident_like()?)
9940        } else {
9941            match self.peek() {
9942                Token::Ident(s) | Token::QuotedIdent(s)
9943                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9944                {
9945                    let a = s.clone();
9946                    self.advance();
9947                    Some(a)
9948                }
9949                _ => None,
9950            }
9951        };
9952        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9953        // through the SAME join grammar the FROM clause uses (see the
9954        // `advance()`-destroys-tokens note on `parse_from_joins`).
9955        let mut mysql_on: Option<Expr> = None;
9956        let mut mysql_outer = false;
9957        let mysql_using = if mysql_pre_target.is_some()
9958            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9959        {
9960            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9961            let mut joins = self.parse_from_joins(&target_qual)?;
9962            if joins.is_empty() {
9963                return Err(self.err(alloc::string::String::from(
9964                    "multi-table DELETE needs at least one source table",
9965                )));
9966            }
9967            let head = joins.remove(0);
9968            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9969            mysql_on = head.on;
9970            Some(FromClause {
9971                primary: head.table,
9972                joins,
9973            })
9974        } else {
9975            None
9976        };
9977        // The pre-FROM target must be the table the FROM names (or its
9978        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9979        // is not the scan target.
9980        if let Some(t) = &mysql_pre_target {
9981            let names_target = t.eq_ignore_ascii_case(&table)
9982                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9983            if !names_target {
9984                return Err(self.err(alloc::format!(
9985                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9986                )));
9987            }
9988        }
9989        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9990        // delete. Same lowering as UPDATE … FROM: the WHERE
9991        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9992        // target row by the correlated machinery.
9993        let using_clause = if let Some(fc) = mysql_using {
9994            Some(fc)
9995        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9996            self.advance();
9997            let mut fc = self.parse_from_clause()?;
9998            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9999            // repeats the TARGET as the first USING entry (PG's spelling
10000            // lists only the extra sources). Peel it so the source subquery
10001            // does not re-scan — and shadow — the target table.
10002            let primary_is_target =
10003                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
10004            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
10005                let head = fc.joins.remove(0);
10006                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
10007                mysql_on = head.on;
10008                fc = FromClause {
10009                    primary: head.table,
10010                    joins: fc.joins,
10011                };
10012            }
10013            Some(fc)
10014        } else {
10015            None
10016        };
10017        let where_ = if matches!(self.peek(), Token::Where) {
10018            self.advance();
10019            Some(self.parse_expr(0)?)
10020        } else {
10021            None
10022        };
10023        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
10024        // read before RETURNING (MariaDB's own extension trails the LIMIT).
10025        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
10026        let mut returning = self.parse_optional_returning()?;
10027        let where_ = if let Some(fc) = using_clause {
10028            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
10029            // a USING-table reference in RETURNING becomes a correlated
10030            // scalar subquery over the USING list.
10031            let names: Vec<String> = core::iter::once(&fc.primary)
10032                .chain(fc.joins.iter().map(|j| &j.table))
10033                .flat_map(|t| {
10034                    t.alias
10035                        .clone()
10036                        .into_iter()
10037                        .chain(core::iter::once(t.name.clone()))
10038                })
10039                .collect();
10040            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
10041            // join filters the SOURCE subquery on the ON predicate alone and
10042            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
10043            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
10044            // rows); every other form folds ON and WHERE into one EXISTS.
10045            let sub_where = match (mysql_on.clone(), where_.clone()) {
10046                _ if mysql_outer => mysql_on.clone(),
10047                (Some(on), Some(w)) => Some(Expr::Binary {
10048                    lhs: Box::new(on),
10049                    op: crate::ast::BinOp::And,
10050                    rhs: Box::new(w),
10051                }),
10052                (Some(on), None) => Some(on),
10053                (None, w) => w,
10054            };
10055            let exists_where = sub_where.clone();
10056            let sub_fc = fc.clone();
10057            let make_subq = move |leaf: Expr| -> Expr {
10058                Expr::ScalarSubquery(Box::new(SelectStatement {
10059                    locking: None,
10060                    ctes: Vec::new(),
10061                    distinct: false,
10062                    distinct_on: Vec::new(),
10063                    items: alloc::vec![SelectItem::Expr {
10064                        expr: leaf,
10065                        alias: None,
10066                    }],
10067                    from: Some(sub_fc.clone()),
10068                    where_: sub_where.clone(),
10069                    group_by: None,
10070                    group_by_all: false,
10071                    having: None,
10072                    unions: Vec::new(),
10073                    order_by: Vec::new(),
10074                    limit: None,
10075                    offset: None,
10076                    limit_with_ties: false,
10077                    window_check_exprs: Vec::new(),
10078                }))
10079            };
10080            let refs = |e: &Expr| expr_refs_tables(e, &names);
10081            if let Some(items) = returning.as_mut() {
10082                for item in items.iter_mut() {
10083                    if let SelectItem::Expr { expr, .. } = item {
10084                        wrap_from_leaves(expr, &names, &make_subq, &refs);
10085                    }
10086                }
10087            }
10088            // A LEFT join deletes the target rows the WHERE selects, reading
10089            // source columns through the correlated subquery (NULL when
10090            // unmatched); no EXISTS row filter.
10091            if mysql_outer {
10092                let mut outer = where_;
10093                if let Some(w) = outer.as_mut() {
10094                    wrap_from_leaves(w, &names, &make_subq, &refs);
10095                }
10096                outer
10097            } else {
10098                Some(Expr::Exists {
10099                    subquery: Box::new(SelectStatement {
10100                        locking: None,
10101                        ctes: Vec::new(),
10102                        distinct: false,
10103                        distinct_on: Vec::new(),
10104                        items: alloc::vec![SelectItem::Expr {
10105                            expr: Expr::Literal(Literal::Integer(1)),
10106                            alias: None,
10107                        }],
10108                        from: Some(fc),
10109                        where_: exists_where,
10110                        group_by: None,
10111                        group_by_all: false,
10112                        having: None,
10113                        unions: Vec::new(),
10114                        order_by: Vec::new(),
10115                        limit: None,
10116                        offset: None,
10117                        limit_with_ties: false,
10118                        window_check_exprs: Vec::new(),
10119                    }),
10120                    negated: false,
10121                })
10122            }
10123        } else {
10124            where_
10125        };
10126        Ok(Statement::Delete(crate::ast::DeleteStatement {
10127            ctes: Vec::new(),
10128            table,
10129            only,
10130            alias,
10131            where_,
10132            order_limit: delete_order_limit,
10133            returning,
10134        }))
10135    }
10136
10137    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
10138    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
10139    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
10140    /// keyword. v7.17 surface:
10141    ///   * source: table reference (subquery source is a follow-up)
10142    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
10143    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
10144    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
10145    ///     order
10146    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
10147        // INTO
10148        let is_into_kw = matches!(self.peek(), Token::Into)
10149            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
10150        if !is_into_kw {
10151            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
10152        }
10153        self.advance();
10154        let target = self.expect_ident_like()?;
10155        // Optional alias — bare ident before USING.
10156        let target_alias = match self.peek() {
10157            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
10158                Some(self.expect_ident_like()?)
10159            }
10160            _ => None,
10161        };
10162        // USING
10163        let is_using_kw = matches!(
10164            self.peek(),
10165            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
10166        );
10167        if !is_using_kw {
10168            return Err(self.err(format!(
10169                "expected USING after MERGE INTO target, got {:?}",
10170                self.peek()
10171            )));
10172        }
10173        self.advance();
10174        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
10175        // <table> [alias]`. PG requires an alias after a subquery source.
10176        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
10177            self.advance(); // (
10178            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
10179            // constant-SELECT lowering the derived-table parser uses
10180            // (PG deletes through this form; it was a parse error).
10181            let inner = if matches!(self.peek(), Token::Values) {
10182                self.advance(); // VALUES
10183                Statement::Select(self.parse_values_rows_body()?)
10184            } else {
10185                self.parse_select_stmt()?
10186            };
10187            match self.advance() {
10188                Token::RParen => {}
10189                other => {
10190                    return Err(self.err(format!(
10191                        "expected ')' after MERGE USING subquery, got {other:?}"
10192                    )));
10193                }
10194            }
10195            let Statement::Select(sub) = inner else {
10196                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
10197            };
10198            (String::new(), Some(Box::new(sub)))
10199        } else {
10200            (self.expect_ident_like()?, None)
10201        };
10202        let source_alias = match self.peek() {
10203            Token::Ident(s) | Token::QuotedIdent(s)
10204                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
10205            {
10206                Some(self.expect_ident_like()?)
10207            }
10208            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
10209                self.advance(); // AS
10210                Some(self.expect_ident_like()?)
10211            }
10212            _ => None,
10213        };
10214        // v7.39 (round 768, F31-D5) — optional positional column-alias
10215        // list after the source alias (`s(id, v)`).
10216        let mut source_column_aliases: Vec<String> = Vec::new();
10217        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
10218            self.advance();
10219            loop {
10220                source_column_aliases.push(self.expect_ident_like()?);
10221                match self.peek() {
10222                    Token::Comma => {
10223                        self.advance();
10224                    }
10225                    Token::RParen => {
10226                        self.advance();
10227                        break;
10228                    }
10229                    other => {
10230                        return Err(self.err(format!(
10231                            "expected ',' or ')' in MERGE source column list, got {other:?}"
10232                        )));
10233                    }
10234                }
10235            }
10236        }
10237        if source_select.is_some() && source_alias.is_none() {
10238            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
10239        }
10240        // ON
10241        if !matches!(self.peek(), Token::On) {
10242            return Err(self.err(format!(
10243                "expected ON after MERGE … USING source, got {:?}",
10244                self.peek()
10245            )));
10246        }
10247        self.advance();
10248        let on = self.parse_expr(0)?;
10249        // One or more WHEN clauses.
10250        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
10251        loop {
10252            let is_when_kw = matches!(
10253                self.peek(),
10254                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
10255            );
10256            if !is_when_kw {
10257                break;
10258            }
10259            self.advance(); // WHEN
10260            // [NOT] MATCHED
10261            let matched = if matches!(self.peek(), Token::Not) {
10262                self.advance();
10263                crate::ast::MergeMatched::NotMatched
10264            } else {
10265                crate::ast::MergeMatched::Matched
10266            };
10267            let is_matched_kw = matches!(
10268                self.peek(),
10269                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
10270            );
10271            if !is_matched_kw {
10272                return Err(self.err(format!(
10273                    "expected MATCHED in WHEN clause, got {:?}",
10274                    self.peek()
10275                )));
10276            }
10277            self.advance();
10278            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
10279            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
10280            // to fire for target rows no source row matches.
10281            let mut matched = matched;
10282            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
10283                self.advance();
10284                match self.peek() {
10285                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
10286                        self.advance();
10287                        matched = crate::ast::MergeMatched::NotMatchedBySource;
10288                    }
10289                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
10290                        self.advance();
10291                    }
10292                    other => {
10293                        return Err(self.err(format!(
10294                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
10295                        )));
10296                    }
10297                }
10298            }
10299            // Optional AND <expr>
10300            let condition = if matches!(self.peek(), Token::And) {
10301                self.advance();
10302                Some(self.parse_expr(0)?)
10303            } else {
10304                None
10305            };
10306            // THEN
10307            let is_then_kw = matches!(
10308                self.peek(),
10309                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10310            );
10311            if !is_then_kw {
10312                return Err(self.err(format!(
10313                    "expected THEN in WHEN clause, got {:?}",
10314                    self.peek()
10315                )));
10316            }
10317            self.advance();
10318            // Action: INSERT / UPDATE / DELETE / DO NOTHING
10319            let action = match self.peek().clone() {
10320                Token::Insert => {
10321                    self.advance();
10322                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10323                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10324                    // VALUES (…)` omits it and fills every column in declaration
10325                    // order. PG accepts this; SPG used to require the list.
10326                    let mut columns: Vec<String> = Vec::new();
10327                    if matches!(self.peek(), Token::LParen) {
10328                        self.advance();
10329                        loop {
10330                            columns.push(self.expect_ident_like()?);
10331                            if matches!(self.peek(), Token::Comma) {
10332                                self.advance();
10333                                continue;
10334                            }
10335                            break;
10336                        }
10337                        if !matches!(self.peek(), Token::RParen) {
10338                            return Err(self.err(format!(
10339                                "expected ')' after INSERT column list, got {:?}",
10340                                self.peek()
10341                            )));
10342                        }
10343                        self.advance();
10344                    }
10345                    // VALUES (...)
10346                    if !matches!(self.peek(), Token::Values) {
10347                        return Err(self.err(format!(
10348                            "expected VALUES in MERGE INSERT, got {:?}",
10349                            self.peek()
10350                        )));
10351                    }
10352                    self.advance();
10353                    if !matches!(self.peek(), Token::LParen) {
10354                        return Err(self.err(format!(
10355                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
10356                            self.peek()
10357                        )));
10358                    }
10359                    self.advance();
10360                    let mut values: Vec<crate::ast::Expr> = Vec::new();
10361                    loop {
10362                        values.push(self.parse_expr(0)?);
10363                        if matches!(self.peek(), Token::Comma) {
10364                            self.advance();
10365                            continue;
10366                        }
10367                        break;
10368                    }
10369                    if !matches!(self.peek(), Token::RParen) {
10370                        return Err(self.err(format!(
10371                            "expected ')' after MERGE INSERT values, got {:?}",
10372                            self.peek()
10373                        )));
10374                    }
10375                    self.advance();
10376                    // Empty column list = positional into every column, so the
10377                    // count is checked against the table arity at execution.
10378                    if !columns.is_empty() && columns.len() != values.len() {
10379                        return Err(self.err(format!(
10380                            "MERGE INSERT column count ({}) ≠ value count ({})",
10381                            columns.len(),
10382                            values.len()
10383                        )));
10384                    }
10385                    crate::ast::MergeAction::Insert { columns, values }
10386                }
10387                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10388                    self.advance();
10389                    // SET
10390                    let is_set_kw = matches!(
10391                        self.peek(),
10392                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10393                    );
10394                    if !is_set_kw {
10395                        return Err(self.err(format!(
10396                            "expected SET after UPDATE in MERGE, got {:?}",
10397                            self.peek()
10398                        )));
10399                    }
10400                    self.advance();
10401                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10402                    loop {
10403                        let col = self.expect_ident_like()?;
10404                        if !matches!(self.peek(), Token::Eq) {
10405                            return Err(self.err(format!(
10406                                "expected '=' in MERGE UPDATE assignment, got {:?}",
10407                                self.peek()
10408                            )));
10409                        }
10410                        self.advance();
10411                        let expr = self.parse_expr(0)?;
10412                        assignments.push((col, expr));
10413                        if matches!(self.peek(), Token::Comma) {
10414                            self.advance();
10415                            continue;
10416                        }
10417                        break;
10418                    }
10419                    crate::ast::MergeAction::Update { assignments }
10420                }
10421                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10422                    self.advance();
10423                    crate::ast::MergeAction::Delete
10424                }
10425                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10426                    self.advance();
10427                    let is_nothing_kw = matches!(
10428                        self.peek(),
10429                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10430                    );
10431                    if !is_nothing_kw {
10432                        return Err(self.err(format!(
10433                            "expected NOTHING after DO in MERGE clause, got {:?}",
10434                            self.peek()
10435                        )));
10436                    }
10437                    self.advance();
10438                    crate::ast::MergeAction::DoNothing
10439                }
10440                other => {
10441                    return Err(self.err(format!(
10442                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10443                    )));
10444                }
10445            };
10446            // PG's grammar simply has no INSERT production under BY SOURCE
10447            // (a target row already exists there) — same syntax error.
10448            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10449                && matches!(action, crate::ast::MergeAction::Insert { .. })
10450            {
10451                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10452            }
10453            clauses.push(crate::ast::MergeWhenClause {
10454                matched,
10455                condition,
10456                action,
10457            });
10458        }
10459        if clauses.is_empty() {
10460            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10461        }
10462        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10463        // unconditional (no `AND`) WHEN of the same match kind: it could
10464        // never fire. Check per match kind in clause order.
10465        let mut seen_unconditional_matched = false;
10466        let mut seen_unconditional_not_matched = false;
10467        let mut seen_unconditional_by_source = false;
10468        for c in &clauses {
10469            let seen = match c.matched {
10470                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10471                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10472                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10473            };
10474            if *seen {
10475                return Err(self.err(String::from(
10476                    "unreachable WHEN clause specified after unconditional WHEN clause",
10477                )));
10478            }
10479            if c.condition.is_none() {
10480                *seen = true;
10481            }
10482        }
10483        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10484        let returning = self.parse_optional_returning()?;
10485        Ok(Statement::Merge(crate::ast::MergeStatement {
10486            // Attached by `parse_with_cte_then_select` when the MERGE
10487            // heads a WITH clause (round 149).
10488            ctes: Vec::new(),
10489            target,
10490            target_alias,
10491            source,
10492            source_alias,
10493            source_select,
10494            source_column_aliases,
10495            on,
10496            clauses,
10497            returning,
10498        }))
10499    }
10500
10501    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10502    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10503    /// as SELECT, so `RETURNING *`, `RETURNING col`,
10504    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10505    fn parse_optional_returning(
10506        &mut self,
10507    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10508        let is_returning_kw = matches!(
10509            self.peek(),
10510            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10511        );
10512        if !is_returning_kw {
10513            return Ok(None);
10514        }
10515        self.advance();
10516        let mut items = Vec::new();
10517        loop {
10518            items.push(self.parse_select_item()?);
10519            if matches!(self.peek(), Token::Comma) {
10520                self.advance();
10521                continue;
10522            }
10523            break;
10524        }
10525        Ok(Some(items))
10526    }
10527
10528    /// v6.0.4 — parse the tail of an ALTER statement after the
10529    /// leading `ALTER` keyword has been consumed. Only one form is
10530    /// supported in v6.0.4:
10531    ///
10532    /// ```text
10533    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10534    /// ```
10535    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10536        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10537        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10538        // exclusion) is accepted by stripping the `ONLY` keyword
10539        // before the table parse.
10540        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10541        // and the long PG-dump tail are accepted as no-ops.
10542        match self.advance() {
10543            Token::Index => {}
10544            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10545            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10546            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10547            Token::Table => {
10548                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10549                    self.advance();
10550                }
10551                return self.parse_alter_table_after_keyword();
10552            }
10553            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10554                return self.parse_alter_policy_after_keyword();
10555            }
10556            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10557                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10558                    self.advance();
10559                }
10560                return self.parse_alter_table_after_keyword();
10561            }
10562            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10563            // of the silent-noop tail.
10564            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10565                return self.parse_alter_sequence_after_keyword();
10566            }
10567            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10568            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10569            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10570            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10571                // NB: the match arm consumed `TYPE` via self.advance(); the
10572                // cursor is now at the type name — do NOT advance again.
10573                let type_name = self.expect_ident_like()?;
10574                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10575                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10576                if is_add_value {
10577                    self.advance(); // ADD
10578                    self.advance(); // VALUE
10579                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10580                    // IF/EXISTS as identifiers.
10581                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10582                    {
10583                        let n1 = self.tokens.get(self.pos + 1);
10584                        let n2 = self.tokens.get(self.pos + 2);
10585                        if matches!(n1, Some(Token::Not))
10586                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10587                        {
10588                            self.advance();
10589                            self.advance();
10590                            self.advance();
10591                            true
10592                        } else {
10593                            false
10594                        }
10595                    } else {
10596                        false
10597                    };
10598                    let label = self.expect_string_literal()?;
10599                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10600                    {
10601                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10602                        self.advance();
10603                        let anchor = self.expect_string_literal()?;
10604                        Some((is_before, anchor))
10605                    } else {
10606                        None
10607                    };
10608                    return Ok(Statement::AlterTypeAddValue {
10609                        type_name,
10610                        label,
10611                        if_not_exists,
10612                        position,
10613                    });
10614                }
10615                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10616                // Used to fall into the no-op tail below: accepted, silently
10617                // ignored. `RENAME TO <newtype>` keeps falling through.
10618                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10619                    && matches!(
10620                        self.tokens.get(self.pos + 1),
10621                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10622                    )
10623                {
10624                    self.advance(); // RENAME
10625                    self.advance(); // VALUE
10626                    let old = self.expect_string_literal()?;
10627                    if matches!(self.peek(), Token::To) {
10628                        self.advance();
10629                    } else {
10630                        self.expect_keyword_ident("to")?;
10631                    }
10632                    let new = self.expect_string_literal()?;
10633                    return Ok(Statement::AlterTypeRenameValue {
10634                        type_name,
10635                        old,
10636                        new,
10637                    });
10638                }
10639                // Other ALTER TYPE forms — the ACTION stays a no-op
10640                // (pg_dump tail), but v7.39 (round 708) the NAME is
10641                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10642                // success for a type that does not exist.
10643                self.consume_until_statement_boundary();
10644                return Ok(Statement::ValidateOnly {
10645                    kind: crate::ast::ValidateOnlyKind::TypeName,
10646                    names: alloc::vec![type_name],
10647                });
10648            }
10649            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10650            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10651            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10652            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10653            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10654            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10655            // pg_dump no-op list below: every form used to report success
10656            // and change nothing, which is worse than refusing outright
10657            // (a migration dropping a constraint kept rejecting data).
10658            // NOTE: the enclosing `match self.advance()` already consumed
10659            // the DOMAIN keyword, so the name is next.
10660            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10661                return self.parse_alter_domain_after_keyword();
10662            }
10663            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10664            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10665            // used to fall into the pg_dump no-op tail below, so a DBA
10666            // setting a per-role default was told it worked and nothing
10667            // happened. Intercepted here, BEFORE that tail.
10668            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10669            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10670            // interception below exists: swallowed with the no-op tail, an
10671            // unknown parameter name was ACCEPTED where PG18 answers
10672            // `unrecognized configuration parameter`. SPG applies nothing
10673            // either way — there is no postgresql.auto.conf — but it now
10674            // says so about a name it does not know.
10675            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10676                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10677                // already consumed here. An extra advance eats the SET and
10678                // the parameter name is never seen — which is exactly the
10679                // bug a panic in this branch disproved: the branch WAS on
10680                // the path, the reading of it was wrong.
10681                let mut parameter = None;
10682                // SET <name> … | RESET <name> | RESET ALL
10683                if matches!(self.peek(), Token::Ident(k)
10684                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10685                {
10686                    self.advance();
10687                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10688                        && !n.eq_ignore_ascii_case("all")
10689                    {
10690                        self.advance();
10691                        // A dotted GUC (`plpgsql.check_asserts`) is two
10692                        // tokens; keep the whole name.
10693                        let mut full = n;
10694                        while matches!(self.peek(), Token::Dot) {
10695                            self.advance();
10696                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10697                                full.push('.');
10698                                full.push_str(&t);
10699                            }
10700                        }
10701                        parameter = Some(full);
10702                    }
10703                }
10704                self.consume_until_statement_boundary();
10705                return Ok(Statement::AlterSystem { parameter });
10706            }
10707            Token::Ident(s) | Token::QuotedIdent(s)
10708                if matches!(
10709                    s.to_ascii_lowercase().as_str(),
10710                    "role" | "user" | "database"
10711                ) && self.peeks_db_role_setting() =>
10712            {
10713                let is_database = s.eq_ignore_ascii_case("database");
10714                return self.parse_db_role_setting(is_database);
10715            }
10716            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10717            // (the non-SET forms; SET/RESET took the branch above). The
10718            // attributes still no-op — recorded, and the ignored PASSWORD
10719            // is ledgered as its own follow-up — but the ROLE is validated:
10720            // any name was accepted for a role that does not exist.
10721            Token::Ident(s) | Token::QuotedIdent(s)
10722                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10723            {
10724                // NB: the enclosing `match self.advance()` already consumed
10725                // ROLE/USER — the round-695 trap, hit again in this round's
10726                // first draft (the name was eaten and WITH parsed as the
10727                // role). The cursor is at the name.
10728                let name = self.expect_ident_or_string()?;
10729                // v7.39 (round 750) — scan the attribute tail for
10730                // PASSWORD. Everything else stays a recorded no-op, but
10731                // a dropped credential rotation is a SECURITY bug:
10732                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10733                // changed nothing, so the old password kept working.
10734                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10735                // NULL` clears the credential.
10736                let mut password: Option<Option<String>> = None;
10737                loop {
10738                    match self.peek() {
10739                        Token::Semicolon | Token::Eof => break,
10740                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10741                            self.advance();
10742                            match self.advance() {
10743                                Token::String(p) => password = Some(Some(p)),
10744                                Token::Null => password = Some(None),
10745                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10746                                    password = Some(None);
10747                                }
10748                                other => {
10749                                    return Err(self.err(alloc::format!(
10750                                        "expected password string or NULL after PASSWORD, got {other:?}"
10751                                    )));
10752                                }
10753                            }
10754                        }
10755                        _ => {
10756                            self.advance();
10757                        }
10758                    }
10759                }
10760                if name.eq_ignore_ascii_case("all") {
10761                    // `ALTER ROLE ALL …` names every role; nothing to check.
10762                    return Ok(Statement::Empty);
10763                }
10764                if let Some(pw) = password {
10765                    return Ok(Statement::AlterRolePassword { name, password: pw });
10766                }
10767                return Ok(Statement::ValidateOnly {
10768                    kind: crate::ast::ValidateOnlyKind::RoleName,
10769                    names: alloc::vec![name],
10770                });
10771            }
10772            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10773            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10774            // list far enough to validate the NAME; the actions still no-op.
10775            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10776            // models none of them and their dumps are rare.)
10777            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10778                let name = self.expect_ident_or_string()?;
10779                self.consume_until_statement_boundary();
10780                return Ok(Statement::ValidateOnly {
10781                    kind: crate::ast::ValidateOnlyKind::CollationName,
10782                    names: alloc::vec![name],
10783                });
10784            }
10785            Token::Ident(s) | Token::QuotedIdent(s)
10786                if s.eq_ignore_ascii_case("text")
10787                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10788                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10789            {
10790                self.advance(); // SEARCH
10791                self.advance(); // CONFIGURATION
10792                let name = self.expect_ident_like()?;
10793                self.consume_until_statement_boundary();
10794                return Ok(Statement::ValidateOnly {
10795                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10796                    names: alloc::vec![name],
10797                });
10798            }
10799            Token::Ident(s) | Token::QuotedIdent(s)
10800                if s.eq_ignore_ascii_case("event")
10801                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10802            {
10803                self.advance(); // TRIGGER
10804                let name = self.expect_ident_like()?;
10805                self.consume_until_statement_boundary();
10806                return Ok(Statement::ValidateOnly {
10807                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10808                    names: alloc::vec![name],
10809                });
10810            }
10811            Token::Ident(s) | Token::QuotedIdent(s)
10812                if s.eq_ignore_ascii_case("large")
10813                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10814            {
10815                self.advance(); // OBJECT
10816                let oid = match self.advance() {
10817                    Token::Integer(n) => alloc::format!("{n}"),
10818                    other => {
10819                        return Err(
10820                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10821                        );
10822                    }
10823                };
10824                self.consume_until_statement_boundary();
10825                return Ok(Statement::ValidateOnly {
10826                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10827                    names: alloc::vec![oid],
10828                });
10829            }
10830            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10831            // argument-list parse as DROP AGGREGATE (round 707); the
10832            // action no-ops, the existence check is real.
10833            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10834                // Same round-695 trap as above: AGGREGATE is already
10835                // consumed; the cursor is at the name.
10836                let name = self.expect_ident_like()?;
10837                let mut names = alloc::vec![name];
10838                if matches!(self.peek(), Token::LParen) {
10839                    self.advance();
10840                    loop {
10841                        match self.peek().clone() {
10842                            Token::RParen => {
10843                                self.advance();
10844                                break;
10845                            }
10846                            Token::Star => {
10847                                self.advance();
10848                                names.push(String::from("*"));
10849                            }
10850                            Token::Comma => {
10851                                self.advance();
10852                            }
10853                            _ => {
10854                                let mut t = self.expect_ident_like()?;
10855                                while let Token::Ident(nx) = self.peek() {
10856                                    let nx = nx.clone();
10857                                    self.advance();
10858                                    t.push(' ');
10859                                    t.push_str(&nx);
10860                                }
10861                                names.push(t);
10862                            }
10863                        }
10864                    }
10865                }
10866                self.consume_until_statement_boundary();
10867                return Ok(Statement::ValidateOnly {
10868                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10869                    names,
10870                });
10871            }
10872            Token::Ident(s) | Token::QuotedIdent(s)
10873                if matches!(
10874                    s.to_ascii_lowercase().as_str(),
10875                    "view"
10876                        | "function"
10877                        | "database"
10878                        | "schema"
10879                        | "owner"
10880                        | "default"
10881                        | "extension"
10882                        | "materialized"
10883                        | "publication"
10884                        | "subscription"
10885                        // v7.37.17 (17.6 siblings) — additional ALTER
10886                        // targets pg_dump / pg_dumpall / operator DB
10887                        // migration scripts commonly emit. SPG has
10888                        // no matching machinery for any of these; the
10889                        // parser accepts + Empty-returns so pg_dump
10890                        // tail statements don't stall.
10891                        | "tablespace"
10892                        | "language"
10893                        | "operator"
10894                        | "conversion"
10895                        | "statistics"
10896                        | "server"
10897                        | "foreign"
10898                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10899                        // / TEMPLATE (CONFIGURATION intercepted above).
10900                        | "text"
10901                ) =>
10902            {
10903                self.consume_until_statement_boundary();
10904                return Ok(Statement::Empty);
10905            }
10906            other => {
10907                return Err(self.err(format!(
10908                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10909                     after ALTER, got {other:?}"
10910                )));
10911            }
10912        }
10913        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10914        // (mailrs migrate-042 ships these). The presence of an
10915        // IF EXISTS makes the subsequent name lookup tolerate
10916        // a missing index — engine returns CommandOk no-op.
10917        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10918            let next = self.tokens.get(self.pos + 1);
10919            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10920                self.advance();
10921                self.advance();
10922                true
10923            } else {
10924                false
10925            }
10926        } else {
10927            false
10928        };
10929        let name = self.expect_ident_like()?;
10930        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10931        // Detect BEFORE the REBUILD path so the existing REBUILD
10932        // arm stays untouched.
10933        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10934            self.advance();
10935            if matches!(self.peek(), Token::To) {
10936                self.advance();
10937            } else {
10938                self.expect_keyword_ident("to")?;
10939            }
10940            let new = self.expect_ident_like()?;
10941            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10942                name,
10943                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10944            }));
10945        }
10946        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10947        // A syntax error before; the index is validated, the params no-op.
10948        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10949            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10950                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10951        {
10952            self.consume_until_statement_boundary();
10953            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10954                name,
10955                target: crate::ast::AlterIndexTarget::StorageParams,
10956            }));
10957        }
10958        // REBUILD
10959        self.expect_keyword_ident("rebuild")?;
10960        // Optional: WITH (encoding = <enc>)
10961        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10962            self.advance();
10963            if !matches!(self.peek(), Token::LParen) {
10964                return Err(self.err(format!(
10965                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10966                    self.peek()
10967                )));
10968            }
10969            self.advance();
10970            self.expect_keyword_ident("encoding")?;
10971            if !matches!(self.peek(), Token::Eq) {
10972                return Err(self.err(format!(
10973                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10974                    self.peek()
10975                )));
10976            }
10977            self.advance();
10978            let enc_ident = match self.advance() {
10979                Token::Ident(s) | Token::QuotedIdent(s) => s,
10980                other => {
10981                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10982                }
10983            };
10984            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10985                "f32" => VecEncoding::F32,
10986                "sq8" => VecEncoding::Sq8,
10987                "half" => VecEncoding::F16,
10988                other => {
10989                    return Err(self.err(format!(
10990                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10991                    )));
10992                }
10993            };
10994            if !matches!(self.peek(), Token::RParen) {
10995                return Err(self.err(format!(
10996                    "expected ')' after encoding value, got {:?}",
10997                    self.peek()
10998                )));
10999            }
11000            self.advance();
11001            Some(enc)
11002        } else {
11003            None
11004        };
11005        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
11006            name,
11007            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
11008        }))
11009    }
11010
11011    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
11012    /// only `SET` form currently supported; future v6.7.x can add
11013    /// more SET subjects without changing the dispatch shape.
11014    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
11015    /// subactions. Single-subaction shape stays a 1-element vec.
11016    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
11017        let table_name = self.expect_ident_like()?;
11018        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
11019        loop {
11020            let subaction = self.parse_alter_table_subaction()?;
11021            // ADD COLUMN with inline REFERENCES emits both an
11022            // AddColumn and an AddForeignKey subaction; the
11023            // helper returns 1 or 2 items.
11024            targets.extend(subaction);
11025            if matches!(self.peek(), Token::Comma) {
11026                self.advance();
11027                continue;
11028            }
11029            break;
11030        }
11031        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
11032            name: table_name,
11033            targets,
11034        }))
11035    }
11036
11037    /// Parse one ALTER TABLE subaction. Returns a Vec because
11038    /// inline `REFERENCES` on `ADD COLUMN` produces both an
11039    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
11040    /// v7.39.9 — MySQL's `FIRST` / `AFTER <col>` trailer on ADD /
11041    /// MODIFY / CHANGE COLUMN. Absent is the PostgreSQL form, which
11042    /// appends.
11043    fn parse_column_position(&mut self) -> Option<crate::ast::ColumnPosition> {
11044        match self.peek() {
11045            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
11046                self.advance();
11047                Some(crate::ast::ColumnPosition::First)
11048            }
11049            Token::Ident(s) if s.eq_ignore_ascii_case("after") => {
11050                self.advance();
11051                let name = self.expect_ident_like().ok()?;
11052                Some(crate::ast::ColumnPosition::After(name))
11053            }
11054            _ => None,
11055        }
11056    }
11057
11058    fn parse_alter_table_subaction(
11059        &mut self,
11060    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11061        match self.peek() {
11062            // v7.39.9 — MySQL's own ALTER TABLE vocabulary. Each one is
11063            // a statement a real migration emits and SPG answered 1064
11064            // for; measured against MySQL 9.7.2, one at a time, beside
11065            // the published image.
11066            Token::Ident(s)
11067                if s.eq_ignore_ascii_case("modify") || s.eq_ignore_ascii_case("change") =>
11068            {
11069                let changing = s.eq_ignore_ascii_case("change");
11070                self.advance();
11071                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("column")) {
11072                    self.advance();
11073                }
11074                // `parse_column_def_with_fk` reads the NAME itself, so
11075                // `MODIFY` hands it the column and `CHANGE` eats the old
11076                // name first and lets it read the new one.
11077                let old_name = if changing {
11078                    Some(self.expect_ident_like()?)
11079                } else {
11080                    None
11081                };
11082                let (definition, _fk) = self.parse_column_def_with_fk()?;
11083                let column = old_name.clone().unwrap_or_else(|| definition.name.clone());
11084                let rename_to = if changing {
11085                    Some(definition.name.clone())
11086                } else {
11087                    None
11088                };
11089                let position = self.parse_column_position();
11090                Ok(alloc::vec![crate::ast::AlterTableTarget::ModifyColumn {
11091                    column,
11092                    rename_to,
11093                    definition,
11094                    position,
11095                }])
11096            }
11097            Token::Ident(s) if s.eq_ignore_ascii_case("auto_increment") => {
11098                self.advance();
11099                if matches!(self.peek(), Token::Eq) {
11100                    self.advance();
11101                }
11102                let n = self.expect_u64_literal()?;
11103                Ok(alloc::vec![
11104                    crate::ast::AlterTableTarget::SetTableAutoIncrement(
11105                        i64::try_from(n).unwrap_or(i64::MAX)
11106                    )
11107                ])
11108            }
11109            Token::Ident(s) if s.eq_ignore_ascii_case("engine") => {
11110                self.advance();
11111                if matches!(self.peek(), Token::Eq) {
11112                    self.advance();
11113                }
11114                // v7.39.10 — as WRITTEN, the way `CREATE TABLE`'s ENGINE
11115                // clause has kept it since v7.39.3. The lexer folds a
11116                // bare identifier, and MySQL names the engine back
11117                // exactly: measured, `ALTER TABLE f1 ENGINE=NoSuchEng`
11118                // answers `Unknown storage engine 'NoSuchEng'` there and
11119                // answered `'nosucheng'` here — the one thing that
11120                // message is for is telling the operator which word in
11121                // their migration was wrong.
11122                let at = self.pos;
11123                let name = self.expect_ident_like()?;
11124                let written = self
11125                    .source_span(at, at)
11126                    .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
11127                    .filter(|raw| raw.eq_ignore_ascii_case(&name))
11128                    .map(alloc::string::String::from);
11129                Ok(alloc::vec![crate::ast::AlterTableTarget::SetEngine(
11130                    written.unwrap_or(name)
11131                )])
11132            }
11133            Token::Ident(s) if s.eq_ignore_ascii_case("convert") => {
11134                self.advance();
11135                // CONVERT TO CHARACTER SET <cs> [COLLATE <c>]
11136                if matches!(self.peek(), Token::To) {
11137                    self.advance();
11138                }
11139                let kw = self.expect_ident_like()?;
11140                if !kw.eq_ignore_ascii_case("character") {
11141                    return Err(self.err("expected CHARACTER after CONVERT TO".into()));
11142                }
11143                let set_kw = self.expect_ident_like()?;
11144                if !set_kw.eq_ignore_ascii_case("set") {
11145                    return Err(self.err("expected SET after CHARACTER".into()));
11146                }
11147                let charset = self.expect_ident_like()?;
11148                let collate =
11149                    if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("collate")) {
11150                        self.advance();
11151                        Some(self.expect_ident_like()?)
11152                    } else {
11153                        None
11154                    };
11155                Ok(alloc::vec![
11156                    crate::ast::AlterTableTarget::ConvertToCharacterSet { charset, collate }
11157                ])
11158            }
11159            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11160                self.advance();
11161                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
11162                // storage parameters: paren-prefixed; consume.
11163                if matches!(self.peek(), Token::LParen) {
11164                    self.consume_until_statement_boundary();
11165                    return Ok(Vec::new());
11166                }
11167                let setting = self.expect_ident_like()?;
11168                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
11169                    if !matches!(self.peek(), Token::Eq) {
11170                        return Err(self.err(alloc::format!(
11171                            "expected '=' after hot_tier_bytes, got {:?}",
11172                            self.peek()
11173                        )));
11174                    }
11175                    self.advance();
11176                    let n = self.expect_u64_literal()?;
11177                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
11178                }
11179                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
11180                // accept-and-no-op for ALTER TABLE SET <subject>
11181                // forms that pg_dump emits but SPG either treats
11182                // as N/A (single-tenant, single-owner, no shared
11183                // tablespaces) or accepts the dump-side declaration
11184                // without runtime effect:
11185                //   SET SCHEMA <name>            (18.11)
11186                //   SET TABLESPACE <name>        (18.8)
11187                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
11188                //   SET WITHOUT CLUSTER          (18.13)
11189                //   SET WITHOUT OIDS             (PG legacy)
11190                //   SET (option = value, …)      (storage parameters)
11191                //   SET REPLICA IDENTITY {…}     (18.14)
11192                if setting.eq_ignore_ascii_case("schema")
11193                    || setting.eq_ignore_ascii_case("tablespace")
11194                    || setting.eq_ignore_ascii_case("logged")
11195                    || setting.eq_ignore_ascii_case("unlogged")
11196                    || setting.eq_ignore_ascii_case("without")
11197                {
11198                    self.consume_until_statement_boundary();
11199                    return Ok(Vec::new());
11200                }
11201                if setting.eq_ignore_ascii_case("replica") {
11202                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
11203                    self.consume_until_statement_boundary();
11204                    return Ok(Vec::new());
11205                }
11206                // SET (option=value, …) — storage parameters.
11207                if matches!(self.peek(), Token::LParen) {
11208                    self.consume_until_statement_boundary();
11209                    return Ok(Vec::new());
11210                }
11211                Err(self.err(alloc::format!(
11212                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
11213                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
11214                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
11215                )))
11216            }
11217            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
11218            // not ignored: round 645 gave SPG the inheritance the
11219            // v7.37.18 no-op said it did not have.
11220            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
11221                self.advance();
11222                let parent = self.expect_ident_like()?;
11223                self.consume_until_statement_boundary();
11224                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11225                    parent,
11226                    detach: false
11227                }])
11228            }
11229            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
11230            // LEVEL SECURITY`, which has its own RLS arm below — without
11231            // the guard this swallowed NO FORCE as a no-op.
11232            Token::Ident(s)
11233                if s.eq_ignore_ascii_case("no")
11234                    && !matches!(
11235                        self.tokens.get(self.pos + 1),
11236                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11237                    ) =>
11238            {
11239                self.advance();
11240                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
11241                    if k.eq_ignore_ascii_case("inherit"))
11242                {
11243                    self.advance();
11244                    let parent = self.expect_ident_like()?;
11245                    self.consume_until_statement_boundary();
11246                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11247                        parent,
11248                        detach: true
11249                    }]);
11250                }
11251                self.consume_until_statement_boundary();
11252                Ok(Vec::new())
11253            }
11254            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
11255            // single-owner, so there is still nothing to record.
11256            //
11257            // v7.39 (round 652) — but the name now reaches the engine,
11258            // which refuses a role that does not exist as PG does. The
11259            // no-op was swallowing the whole statement, so a dump naming
11260            // a role this server never heard of restored clean and left
11261            // the table owned by whoever ran the restore.
11262            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
11263                self.advance();
11264                if matches!(self.peek(), Token::To) {
11265                    self.advance();
11266                }
11267                let role = self.expect_ident_like()?;
11268                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
11269                    role
11270                }])
11271            }
11272            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
11273            // PG sets a hint; SPG doesn't have clustered storage, so the
11274            // hint itself stays a no-op.
11275            //
11276            // v7.39 (round 652) — the index name is checked now. PG
11277            // errors on one that does not exist, and swallowing that let
11278            // a typo'd CLUSTER ON pass silently.
11279            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
11280                self.advance();
11281                // `ON` is a reserved token, not an ident.
11282                if !matches!(self.peek(), Token::On) {
11283                    return Err(self.err(alloc::format!(
11284                        "expected ON after CLUSTER, got {:?}",
11285                        self.peek()
11286                    )));
11287                }
11288                self.advance();
11289                let index = self.expect_ident_like()?;
11290                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
11291                    index: Some(index)
11292                }])
11293            }
11294            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
11295            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
11296            // what a logical decoder puts in the old-tuple image; SPG's
11297            // replication is SQL-text, so there is nothing to record.
11298            // Accept-and-no-op (it used to be a parse error).
11299            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
11300                self.advance();
11301                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
11302                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
11303                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
11304                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
11305                {
11306                    self.advance(); // IDENTITY
11307                    self.advance(); // USING
11308                    if matches!(self.peek(), Token::Index)
11309                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
11310                    {
11311                        self.advance();
11312                    }
11313                    let index = self.expect_ident_like()?;
11314                    self.consume_until_statement_boundary();
11315                    return Ok(alloc::vec![
11316                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
11317                    ]);
11318                }
11319                self.consume_until_statement_boundary();
11320                Ok(Vec::new())
11321            }
11322            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
11323            //
11324            // v7.39 (round 652) — it used to consume the statement and
11325            // return nothing, on the stated theory that SPG validated at
11326            // ADD CONSTRAINT time so there was never anything left to
11327            // validate. Measured against PG18, ADD CONSTRAINT did not
11328            // scan the existing rows at all — the comment described a
11329            // property SPG did not have, which is why nobody looked. Both
11330            // halves are real now: ADD scans unless told NOT VALID, and
11331            // this scans what NOT VALID skipped.
11332            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
11333                self.advance();
11334                self.expect_keyword_ident("constraint")?;
11335                let name = self.expect_ident_like()?;
11336                Ok(alloc::vec![
11337                    crate::ast::AlterTableTarget::ValidateConstraint { name }
11338                ])
11339            }
11340            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
11341            // SET (option = value, …). PG uses it to clear per-table
11342            // storage params like fillfactor or autovacuum_*. SPG
11343            // engine-manages those parameters; accept-and-no-op.
11344            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
11345                self.advance();
11346                self.consume_until_statement_boundary();
11347                Ok(Vec::new())
11348            }
11349            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
11350            // type-of binding (PG 9.0+). SPG composite types
11351            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
11352            // TABLE OF is rare and inverse of CREATE TABLE OF.
11353            // Accept-and-no-op until a customer dump round-trips it.
11354            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
11355                self.advance();
11356                // v7.39 (round 710) — the type name is validated now.
11357                let type_name = self.expect_ident_like()?;
11358                self.consume_until_statement_boundary();
11359                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
11360                    type_name
11361                }])
11362            }
11363            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
11364            // (reserved keyword) rather than Token::Ident("not"),
11365            // so it needs its own arm. Accept-and-no-op same as OF.
11366            Token::Not => {
11367                self.advance();
11368                self.consume_until_statement_boundary();
11369                Ok(Vec::new())
11370            }
11371            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
11372            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
11373                self.advance();
11374                self.expect_row_level_security()?;
11375                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11376                    enabled: None,
11377                    force: Some(true),
11378                }])
11379            }
11380            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
11381            Token::Ident(s)
11382                if s.eq_ignore_ascii_case("no")
11383                    && matches!(
11384                        self.tokens.get(self.pos + 1),
11385                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11386                    ) =>
11387            {
11388                self.advance(); // NO
11389                self.advance(); // FORCE
11390                self.expect_row_level_security()?;
11391                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11392                    enabled: None,
11393                    force: Some(false),
11394                }])
11395            }
11396            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
11397            // (sets relrowsecurity). The guard requires the next token to be
11398            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
11399            Token::Ident(s)
11400                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
11401                    && matches!(
11402                        self.tokens.get(self.pos + 1),
11403                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
11404                    ) =>
11405            {
11406                let enabled = s.eq_ignore_ascii_case("enable");
11407                self.advance(); // ENABLE/DISABLE
11408                self.expect_row_level_security()?;
11409                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11410                    enabled: Some(enabled),
11411                    force: None,
11412                }])
11413            }
11414            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11415                self.advance();
11416                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
11417                // {INDEX|KEY} [name] (cols)`, which every ORM migration
11418                // emits. The same grammar CREATE TABLE already accepts
11419                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11420                // through the SAME parser — an ALTER-only copy would be a
11421                // second place for the two to drift.
11422                if self.peek_mysql_inline_key_start() {
11423                    return Ok(match self.parse_mysql_inline_key()? {
11424                        Some(c) => {
11425                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11426                        }
11427                        // FULLTEXT / SPATIAL parse and are accepted as a
11428                        // no-op here exactly as they are inline.
11429                        None => Vec::new(),
11430                    });
11431                }
11432                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11433                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11434                // PRIMARY KEY this way; mysqldump emits both.
11435                // Peek-only dispatch (no advance) — `advance()`
11436                // destructively replaces consumed tokens with Eof,
11437                // so saved-pos restore would land on Eofs.
11438                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11439                {
11440                    // The next-but-one ident is the constraint
11441                    // name; the one after THAT is the kind.
11442                    let kind_pos = self.pos + 2;
11443                    let kind = self.tokens.get(kind_pos).cloned();
11444                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11445                    {
11446                        let fk = self.parse_table_level_fk()?;
11447                        return Ok(alloc::vec![
11448                            crate::ast::AlterTableTarget::AddForeignKey(fk)
11449                        ]);
11450                    }
11451                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11452                    {
11453                        self.advance(); // CONSTRAINT
11454                        // v7.39 (read01 round 48) — keep the name; the engine
11455                        // stores it now instead of dropping it on the floor.
11456                        let con_name = self.expect_ident_like()?;
11457                        self.advance(); // PRIMARY
11458                        self.expect_keyword_ident("key")?;
11459                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11460                        // v7.39 (round 711) — the ALTER form carries the
11461                        // timing too (pg_dump writes it here).
11462                        let (deferrable, initially_deferred) =
11463                            self.consume_deferrable_clauses_timed()?;
11464                        return Ok(alloc::vec![
11465                            crate::ast::AlterTableTarget::AddTableConstraint(
11466                                crate::ast::TableConstraint::PrimaryKey {
11467                                    name: Some(con_name),
11468                                    columns: cols,
11469                                    deferrable,
11470                                    initially_deferred,
11471                                }
11472                            )
11473                        ]);
11474                    }
11475                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11476                    {
11477                        self.advance(); // CONSTRAINT
11478                        // v7.39 (read01 round 48) — keep the name.
11479                        let con_name = self.expect_ident_like()?;
11480                        // v7.22 (mailrs round-13 gap 6) — delegate so
11481                        // the optional `NULLS [NOT] DISTINCT` modifier
11482                        // parses here too (pg_dump emits the ALTER
11483                        // form; semantics enforced by the engine
11484                        // since v7.13).
11485                        let mut uc = self.parse_table_level_unique()?;
11486                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11487                            *name = Some(con_name);
11488                        }
11489                        return Ok(alloc::vec![
11490                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11491                        ]);
11492                    }
11493                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11494                    {
11495                        self.advance(); // CONSTRAINT
11496                        // v7.39 (read01 round 48) — keep the name.
11497                        let con_name = self.expect_ident_like()?;
11498                        self.advance(); // CHECK
11499                        if !matches!(self.peek(), Token::LParen) {
11500                            return Err(self.err(alloc::format!(
11501                                "expected '(' after CHECK, got {:?}", self.peek()
11502                            )));
11503                        }
11504                        self.advance();
11505                        let expr = self.parse_expr(0)?;
11506                        if matches!(self.peek(), Token::RParen) {
11507                            self.advance();
11508                        }
11509                        let not_valid = self.parse_not_valid_suffix();
11510                        return Ok(alloc::vec![
11511                            crate::ast::AlterTableTarget::AddTableConstraint(
11512                                crate::ast::TableConstraint::Check {
11513                                    name: Some(con_name),
11514                                    expr,
11515                                    not_valid,
11516                                }
11517                            )
11518                        ]);
11519                    }
11520                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11521                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11522                    // exclusion constraints via this ALTER form.
11523                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11524                    {
11525                        self.advance(); // CONSTRAINT
11526                        let con_name = self.expect_ident_like()?;
11527                        let mut ex = self.parse_table_level_exclude()?;
11528                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11529                            *name = Some(con_name);
11530                        }
11531                        return Ok(alloc::vec![
11532                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11533                        ]);
11534                    }
11535                    // Unknown kind — fall through to FK path which
11536                    // produces a descriptive parse error.
11537                }
11538                let is_fk = matches!(
11539                    self.peek(),
11540                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11541                        || s.eq_ignore_ascii_case("foreign")
11542                );
11543                if is_fk {
11544                    let fk = self.parse_table_level_fk()?;
11545                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11546                }
11547                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11548                // (no CONSTRAINT prefix) — same dispatch.
11549                match self.peek().clone() {
11550                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11551                        self.advance();
11552                        self.expect_keyword_ident("key")?;
11553                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11554                        let (deferrable, initially_deferred) =
11555                            self.consume_deferrable_clauses_timed()?;
11556                        return Ok(alloc::vec![
11557                            crate::ast::AlterTableTarget::AddTableConstraint(
11558                                crate::ast::TableConstraint::PrimaryKey {
11559                                    name: None,
11560                                    columns: cols,
11561                                    deferrable,
11562                                    initially_deferred,
11563                                }
11564                            )
11565                        ]);
11566                    }
11567                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11568                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
11569                        let uc = self.parse_table_level_unique()?;
11570                        return Ok(alloc::vec![
11571                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11572                        ]);
11573                    }
11574                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11575                    // prefix). The other three bare forms were here and
11576                    // this one was not, so it fell through to the column
11577                    // path and came back as "unexpected reserved keyword
11578                    // 'check' at start of column definition".
11579                    _ if self.peek_table_level_check_start() => {
11580                        let chk = self.parse_table_level_check()?;
11581                        let not_valid = self.parse_not_valid_suffix();
11582                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11583                            unreachable!("parse_table_level_check returns Check")
11584                        };
11585                        return Ok(alloc::vec![
11586                            crate::ast::AlterTableTarget::AddTableConstraint(
11587                                crate::ast::TableConstraint::Check {
11588                                    name: None,
11589                                    expr,
11590                                    not_valid,
11591                                }
11592                            )
11593                        ]);
11594                    }
11595                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11596                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11597                        let ex = self.parse_table_level_exclude()?;
11598                        return Ok(alloc::vec![
11599                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11600                        ]);
11601                    }
11602                    _ => {}
11603                }
11604                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11605                    self.advance();
11606                }
11607                let mut if_not_exists = false;
11608                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11609                    self.advance();
11610                    if !matches!(self.peek(), Token::Not) {
11611                        return Err(self.err(alloc::format!(
11612                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11613                            self.peek()
11614                        )));
11615                    }
11616                    self.advance();
11617                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11618                        return Err(self.err(alloc::format!(
11619                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11620                            self.peek()
11621                        )));
11622                    }
11623                    self.advance();
11624                    if_not_exists = true;
11625                }
11626                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11627                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11628                // returns ColumnDef + an optional inline FK.
11629                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11630                let col_name = column.name.clone();
11631                // v7.39.9 — MySQL says where the column goes.
11632                let position = self.parse_column_position();
11633                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11634                    column,
11635                    if_not_exists,
11636                    position,
11637                }];
11638                if let Some(mut fk) = col_level_fk {
11639                    if fk.columns.is_empty() {
11640                        fk.columns.push(col_name);
11641                    }
11642                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11643                }
11644                Ok(out)
11645            }
11646            Token::Drop => {
11647                self.advance();
11648                // v7.13.3 — dispatch on the next token. mailrs round-7
11649                // S8 closed DROP COLUMN; round-6 S7 closed
11650                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11651                // RESTRICT modifiers.
11652                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11653                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11654                let subject = match self.peek() {
11655                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11656                        self.advance();
11657                        "constraint"
11658                    }
11659                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11660                        self.advance();
11661                        "column"
11662                    }
11663                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11664                    // `INDEX` lexes as the reserved Token::Index, so it is
11665                    // unambiguous. `KEY` is a plain ident, and PG allows a
11666                    // column literally named "key", so only read it as the
11667                    // keyword when a name follows it.
11668                    Token::Index => {
11669                        self.advance();
11670                        "index"
11671                    }
11672                    Token::Ident(s)
11673                        if s.eq_ignore_ascii_case("key")
11674                            && matches!(
11675                                self.tokens.get(self.pos + 1),
11676                                Some(Token::Ident(_) | Token::QuotedIdent(_))
11677                            ) =>
11678                    {
11679                        self.advance();
11680                        "index"
11681                    }
11682                    // PG-canonical bare `DROP <col>` without COLUMN
11683                    // keyword is also valid; treat any other ident
11684                    // as the column name.
11685                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11686                    other => {
11687                        return Err(self.err(alloc::format!(
11688                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11689                        )));
11690                    }
11691                };
11692                let mut if_exists = false;
11693                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11694                    let n1 = self.tokens.get(self.pos + 1);
11695                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11696                        self.advance();
11697                        self.advance();
11698                        if_exists = true;
11699                    }
11700                }
11701                let name = self.expect_ident_like()?;
11702                let mut cascade = false;
11703                if matches!(
11704                    self.peek(),
11705                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11706                        || s.eq_ignore_ascii_case("restrict")
11707                ) {
11708                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11709                    {
11710                        cascade = true;
11711                    }
11712                    self.advance();
11713                }
11714                if subject == "index" {
11715                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11716                        name,
11717                        if_exists,
11718                    }])
11719                } else if subject == "constraint" {
11720                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11721                        name,
11722                        if_exists,
11723                    }])
11724                } else {
11725                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11726                        column: name,
11727                        if_exists,
11728                        cascade,
11729                    }])
11730                }
11731            }
11732            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11733                self.advance();
11734                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11735                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11736                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11737                // immediately; accept-and-no-op.
11738                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11739                    self.advance();
11740                    self.consume_until_statement_boundary();
11741                    return Ok(Vec::new());
11742                }
11743                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11744                    self.advance();
11745                }
11746                let col_name = self.expect_ident_like()?;
11747                match self.peek() {
11748                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11749                        self.advance();
11750                    }
11751                    // v7.14.0 — pg_dump emits BIGSERIAL via
11752                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11753                    // nextval('seq')` (the sequence is created
11754                    // separately). SPG's BIGSERIAL already uses
11755                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11756                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11757                    // engine no-ops by consuming the tail.
11758                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11759                        // v7.22 (round-13 T2) — `SET DEFAULT
11760                        // nextval('…')` is how pg_dump spells a
11761                        // SERIAL column (plain integer in CREATE
11762                        // TABLE + this ALTER). It used to be
11763                        // swallowed as a no-op, which silently
11764                        // STRIPPED auto-increment from imported
11765                        // schemas — the first post-import INSERT
11766                        // without an explicit id then violated NOT
11767                        // NULL. Lower it to the auto-increment
11768                        // marker instead.
11769                        let is_default_nextval =
11770                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11771                                && matches!(
11772                                    self.tokens.get(self.pos + 2),
11773                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11774                                );
11775                        if is_default_nextval {
11776                            let seq_name = self.scan_sequence_name_until_boundary();
11777                            return Ok(alloc::vec![
11778                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11779                                    column: col_name,
11780                                    seq_name,
11781                                }
11782                            ]);
11783                        }
11784                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11785                        self.advance(); // consume "set"
11786                        match self.peek().clone() {
11787                            Token::Default => {
11788                                self.advance();
11789                                let default_expr = self.parse_expr(0)?;
11790                                return Ok(alloc::vec![
11791                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11792                                        column: col_name,
11793                                        default_expr,
11794                                    }
11795                                ]);
11796                            }
11797                            Token::Not => {
11798                                self.advance();
11799                                if !matches!(self.peek(), Token::Null) {
11800                                    return Err(self.err(alloc::format!(
11801                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11802                                        self.peek()
11803                                    )));
11804                                }
11805                                self.advance();
11806                                return Ok(alloc::vec![
11807                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11808                                        column: col_name,
11809                                    }
11810                                ]);
11811                            }
11812                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11813                            // stored generated column's expression and
11814                            // recompute existing rows.
11815                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11816                                self.advance(); // EXPRESSION
11817                                if matches!(self.peek(), Token::As) {
11818                                    self.advance();
11819                                }
11820                                let expr = self.parse_expr(0)?;
11821                                return Ok(alloc::vec![
11822                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11823                                        column: col_name,
11824                                        expr,
11825                                    }
11826                                ]);
11827                            }
11828                            other => {
11829                                // Other SET subjects (STATISTICS,
11830                                // STORAGE, COMPRESSION, …) stay no-ops —
11831                                // storage hints with no SPG semantics.
11832                                let _ = other;
11833                                self.consume_until_statement_boundary();
11834                                return Ok(Vec::new());
11835                            }
11836                        }
11837                    }
11838                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11839                        self.advance(); // consume "drop"
11840                        return self.parse_alter_column_drop_tail(col_name);
11841                    }
11842                    Token::Drop => {
11843                        self.advance(); // consume Drop token
11844                        return self.parse_alter_column_drop_tail(col_name);
11845                    }
11846                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11847                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11848                        // GENERATED { ALWAYS | BY DEFAULT } AS
11849                        // IDENTITY ( … )`: pg_dump's spelling for
11850                        // identity columns. Same auto-increment
11851                        // lowering as the nextval default; the
11852                        // sequence options inside the parens are
11853                        // no-ops under SPG's max+1 semantics.
11854                        let is_generated = matches!(
11855                            self.tokens.get(self.pos + 1),
11856                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11857                        );
11858                        if !is_generated {
11859                            return Err(self.err(alloc::format!(
11860                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11861                                self.tokens.get(self.pos + 1)
11862                            )));
11863                        }
11864                        let seq_name = self.scan_sequence_name_until_boundary();
11865                        return Ok(alloc::vec![
11866                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11867                                column: col_name,
11868                                seq_name,
11869                            }
11870                        ]);
11871                    }
11872                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11873                    // column: floor the next allocated value at n (bare
11874                    // RESTART = restart from the start value, 1).
11875                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11876                        self.advance();
11877                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11878                        {
11879                            self.advance();
11880                            let neg = if matches!(self.peek(), Token::Minus) {
11881                                self.advance();
11882                                true
11883                            } else {
11884                                false
11885                            };
11886                            match self.advance() {
11887                                Token::Integer(v) => Some(if neg { -v } else { v }),
11888                                other => {
11889                                    return Err(self.err(alloc::format!(
11890                                        "expected integer after RESTART WITH, got {other:?}"
11891                                    )));
11892                                }
11893                            }
11894                        } else {
11895                            None
11896                        };
11897                        return Ok(alloc::vec![
11898                            crate::ast::AlterTableTarget::AlterColumnRestart {
11899                                column: col_name,
11900                                with,
11901                            }
11902                        ]);
11903                    }
11904                    other => {
11905                        return Err(self.err(alloc::format!(
11906                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11907                        )));
11908                    }
11909                }
11910                // v7.39 (round 713) — the type parser has consumed a
11911                // trailing `COLLATE <name>` since Phase 2.5, and
11912                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11913                // TYPE text COLLATE "C"` parsed clean and changed
11914                // nothing. Keep the clause; the engine re-collates.
11915                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _, _, _) =
11916                    self.parse_type_with_implied_flags()?;
11917                let collation = if coll_explicit {
11918                    coll_name.map(|n| (coll, n))
11919                } else {
11920                    None
11921                };
11922                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11923                {
11924                    self.advance();
11925                    Some(self.parse_expr(0)?)
11926                } else {
11927                    None
11928                };
11929                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11930                    column: col_name,
11931                    new_type,
11932                    using,
11933                    collation,
11934                }])
11935            }
11936            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11937            // PG also supports `RENAME TO new_table` for table-name
11938            // rename; that surface is deferred (pg_dump never emits
11939            // it). If the first post-RENAME ident is `TO`, the user
11940            // is asking for table rename — error with a clear
11941            // message rather than misparsing `TO` as a column name.
11942            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11943                self.advance();
11944                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11945                // table-name rename (mailrs round-10 A.5 — used
11946                // by migrate-042's `RENAME TO email_contacts`).
11947                // `TO` lexes as Token::To.
11948                if matches!(self.peek(), Token::To)
11949                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11950                {
11951                    self.advance();
11952                    let new = self.expect_ident_like()?;
11953                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11954                        new,
11955                    }]);
11956                }
11957                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11958                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11959                    self.advance();
11960                    let old = self.expect_ident_like()?;
11961                    if matches!(self.peek(), Token::To) {
11962                        self.advance();
11963                    } else {
11964                        self.expect_keyword_ident("to")?;
11965                    }
11966                    let new = self.expect_ident_like()?;
11967                    return Ok(alloc::vec![
11968                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11969                    ]);
11970                }
11971                // v7.39.9 — MySQL's `RENAME {INDEX|KEY} old TO new`.
11972                // PostgreSQL renames an index with its own top-level
11973                // `ALTER INDEX`, so this spelling had nowhere to go and
11974                // answered 1064; MySQL 9.7.2 parses it and answers 1176
11975                // when the key is not there.
11976                if matches!(self.peek(), Token::Index)
11977                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key"))
11978                {
11979                    self.advance();
11980                    let old = self.expect_ident_like()?;
11981                    if matches!(self.peek(), Token::To) {
11982                        self.advance();
11983                    } else {
11984                        self.expect_keyword_ident("to")?;
11985                    }
11986                    let new = self.expect_ident_like()?;
11987                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameIndex {
11988                        old,
11989                        new,
11990                    }]);
11991                }
11992                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11993                    self.advance();
11994                }
11995                let old = self.expect_ident_like()?;
11996                // `TO` is a reserved keyword token; accept both
11997                // Token::To and Token::Ident("to") for consistency.
11998                if matches!(self.peek(), Token::To) {
11999                    self.advance();
12000                } else {
12001                    self.expect_keyword_ident("to")?;
12002                }
12003                let new = self.expect_ident_like()?;
12004                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
12005                    old,
12006                    new,
12007                }])
12008            }
12009            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
12010            // { ALL | <name> }`. pg_dump --disable-triggers wraps
12011            // every data block with these. Real disable semantics —
12012            // not no-op — because reload correctness assumes the
12013            // triggers don't fire (rows already carry their
12014            // computed values from prod).
12015            Token::Ident(s)
12016                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
12017            {
12018                let enabled = s.eq_ignore_ascii_case("enable");
12019                self.advance();
12020                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
12021                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
12022                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
12023                // pg_dump output) — anything else falls through to
12024                // the catch-all error below.
12025                // v7.22 (round-13 T3) — mysqldump wraps every data
12026                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
12027                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
12028                // maintains indexes incrementally — engine no-op.
12029                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
12030                    self.advance();
12031                    return Ok(Vec::new());
12032                }
12033                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
12034                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
12035                // to gate triggers on session_replication_role; SPG
12036                // has no replica role, so the prefix is consumed and
12037                // treated identically to the plain ENABLE/DISABLE
12038                // TRIGGER form.
12039                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
12040                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
12041                {
12042                    self.advance();
12043                }
12044                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
12045                    return Err(self.err(alloc::format!(
12046                        "expected TRIGGER after {}, got {:?}",
12047                        if enabled { "ENABLE" } else { "DISABLE" },
12048                        self.peek()
12049                    )));
12050                }
12051                self.advance();
12052                // `ALL` lexes as Token::All (reserved); also
12053                // accept Token::Ident("all") for symmetry.
12054                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
12055                // TRIGGER selectors. USER (= all user triggers) is
12056                // semantically ALL here; REPLICA / ALWAYS gate on
12057                // session_replication_role which SPG doesn't track.
12058                // All map to TriggerSelector::All.
12059                let which = if matches!(self.peek(), Token::All)
12060                    || matches!(self.peek(), Token::Ident(s)
12061                        if s.eq_ignore_ascii_case("all")
12062                            || s.eq_ignore_ascii_case("user")
12063                            || s.eq_ignore_ascii_case("replica")
12064                            || s.eq_ignore_ascii_case("always"))
12065                {
12066                    self.advance();
12067                    crate::ast::TriggerSelector::All
12068                } else {
12069                    let name = self.expect_ident_like()?;
12070                    crate::ast::TriggerSelector::Named(name)
12071                };
12072                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
12073                    which,
12074                    enabled,
12075                }])
12076            }
12077            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
12078            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
12079                self.advance();
12080                if !matches!(self.peek(), Token::Partition)
12081                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12082                        if s.eq_ignore_ascii_case("partition"))
12083                {
12084                    return Err(self.err(alloc::format!(
12085                        "expected PARTITION after ATTACH, got {:?}",
12086                        self.peek()
12087                    )));
12088                }
12089                self.advance();
12090                let child = self.expect_ident_like()?;
12091                let bounds = self.parse_partition_bounds_tail()?;
12092                Ok(alloc::vec![
12093                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
12094                ])
12095            }
12096            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
12097            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
12098                self.advance();
12099                if !matches!(self.peek(), Token::Partition)
12100                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12101                        if s.eq_ignore_ascii_case("partition"))
12102                {
12103                    return Err(self.err(alloc::format!(
12104                        "expected PARTITION after DETACH, got {:?}",
12105                        self.peek()
12106                    )));
12107                }
12108                self.advance();
12109                let child = self.expect_ident_like()?;
12110                let mut concurrently = false;
12111                let mut finalize = false;
12112                loop {
12113                    match self.peek().clone() {
12114                        Token::Ident(s) | Token::QuotedIdent(s)
12115                            if s.eq_ignore_ascii_case("concurrently") =>
12116                        {
12117                            self.advance();
12118                            concurrently = true;
12119                        }
12120                        Token::Ident(s) | Token::QuotedIdent(s)
12121                            if s.eq_ignore_ascii_case("finalize") =>
12122                        {
12123                            self.advance();
12124                            finalize = true;
12125                        }
12126                        _ => break,
12127                    }
12128                }
12129                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
12130                    child,
12131                    concurrently,
12132                    finalize,
12133                }])
12134            }
12135            other => Err(self.err(alloc::format!(
12136                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
12137            ))),
12138        }
12139    }
12140
12141    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
12142    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
12143    /// TABLE … ATTACH PARTITION. Shares the same grammar as
12144    /// `parse_partition_of_tail`'s bounds branch.
12145    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
12146    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
12147    /// lowering each to the respective AlterTableTarget. Any
12148    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
12149    /// no-op via consume_until_statement_boundary.
12150    fn parse_alter_column_drop_tail(
12151        &mut self,
12152        col_name: String,
12153    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
12154        match self.peek().clone() {
12155            Token::Default => {
12156                self.advance();
12157                Ok(alloc::vec![
12158                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
12159                ])
12160            }
12161            Token::Not => {
12162                self.advance();
12163                if !matches!(self.peek(), Token::Null) {
12164                    return Err(self.err(alloc::format!(
12165                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
12166                        self.peek()
12167                    )));
12168                }
12169                self.advance();
12170                Ok(alloc::vec![
12171                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
12172                ])
12173            }
12174            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
12175            // generated column into a plain column.
12176            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
12177                self.advance();
12178                // v7.39 (round 187, U10) — IF EXISTS was consumed but
12179                // dropped, so the engine still errored on a plain
12180                // column; PG's semantics are NOTICE + skip.
12181                let mut if_exists = false;
12182                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
12183                    self.advance();
12184                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
12185                        self.advance();
12186                        if_exists = true;
12187                    }
12188                }
12189                Ok(alloc::vec![
12190                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
12191                        column: col_name,
12192                        if_exists,
12193                    }
12194                ])
12195            }
12196            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
12197            // identity column into a plain column.
12198            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
12199                self.advance();
12200                let mut if_exists = false;
12201                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
12202                    self.advance();
12203                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
12204                        self.advance();
12205                        if_exists = true;
12206                    }
12207                }
12208                Ok(alloc::vec![
12209                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
12210                        column: col_name,
12211                        if_exists,
12212                    }
12213                ])
12214            }
12215            _ => {
12216                self.consume_until_statement_boundary();
12217                Ok(Vec::new())
12218            }
12219        }
12220    }
12221
12222    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
12223    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
12224    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
12225    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
12226    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
12227        let mut opts = crate::ast::CopyOptions::default();
12228        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
12229            return Ok(opts);
12230        }
12231        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
12232            self.advance();
12233        }
12234        if matches!(self.peek(), Token::LParen) {
12235            self.advance();
12236            loop {
12237                self.parse_one_copy_option(&mut opts)?;
12238                match self.peek() {
12239                    Token::Comma => {
12240                        self.advance();
12241                    }
12242                    Token::RParen => {
12243                        self.advance();
12244                        break;
12245                    }
12246                    other => {
12247                        return Err(self.err(alloc::format!(
12248                            "expected ',' or ')' in COPY options, got {other:?}"
12249                        )));
12250                    }
12251                }
12252            }
12253        } else {
12254            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12255                self.parse_one_copy_option(&mut opts)?;
12256            }
12257        }
12258        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12259            return Err(self.err(alloc::format!(
12260                "unexpected token after COPY options: {:?}",
12261                self.peek()
12262            )));
12263        }
12264        Ok(opts)
12265    }
12266
12267    fn parse_one_copy_option(
12268        &mut self,
12269        opts: &mut crate::ast::CopyOptions,
12270    ) -> Result<(), ParseError> {
12271        use crate::ast::CopyFormat;
12272        // The option keyword. NULL lexes as its own token; the rest are
12273        // bare identifiers.
12274        let kw = match self.advance() {
12275            Token::Null => alloc::string::String::from("NULL"),
12276            Token::Ident(s) => s.to_uppercase(),
12277            other => {
12278                return Err(self.err(alloc::format!(
12279                    "expected a COPY option keyword, got {other:?}"
12280                )));
12281            }
12282        };
12283        match kw.as_str() {
12284            "FORMAT" => {
12285                let fmt = self.expect_ident_like()?;
12286                match fmt.to_ascii_uppercase().as_str() {
12287                    "CSV" => opts.format = CopyFormat::Csv,
12288                    "TEXT" => opts.format = CopyFormat::Text,
12289                    other => {
12290                        return Err(self.err(alloc::format!(
12291                            "COPY format \"{}\" not recognized",
12292                            other.to_ascii_lowercase()
12293                        )));
12294                    }
12295                }
12296            }
12297            // Legacy bare format keywords.
12298            "CSV" => opts.format = CopyFormat::Csv,
12299            "TEXT" => opts.format = CopyFormat::Text,
12300            "HEADER" => {
12301                opts.header = match self.peek() {
12302                    Token::True => {
12303                        self.advance();
12304                        true
12305                    }
12306                    Token::False => {
12307                        self.advance();
12308                        false
12309                    }
12310                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
12311                        self.advance();
12312                        true
12313                    }
12314                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
12315                        self.advance();
12316                        false
12317                    }
12318                    // Bare HEADER (no boolean) means HEADER true.
12319                    _ => true,
12320                };
12321            }
12322            // r1066 (7.38 S5.1) — pgbench 14+ loads with
12323            // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
12324            // vacuum bookkeeping on a freshly created/truncated
12325            // table; SPG's per-statement visibility makes it a
12326            // faithful no-op, and rejecting it aborted `pgbench -i`
12327            // against the drop-in. Accept ON/OFF/bare, change nothing.
12328            "FREEZE" => match self.peek() {
12329                Token::True | Token::False => {
12330                    self.advance();
12331                }
12332                Token::Ident(s)
12333                    if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
12334                {
12335                    self.advance();
12336                }
12337                _ => {}
12338            },
12339            "DELIMITER" | "QUOTE" | "ESCAPE" => {
12340                let s = match self.advance() {
12341                    Token::String(s) => s,
12342                    other => {
12343                        return Err(self.err(alloc::format!(
12344                            "COPY {kw} expects a single-character string, got {other:?}"
12345                        )));
12346                    }
12347                };
12348                // v7.39 (round 247) — PG's wording (0A000), keyword in
12349                // lowercase: "COPY delimiter must be a single one-byte
12350                // character".
12351                let one_byte_err = || {
12352                    self.err(alloc::format!(
12353                        "COPY {} must be a single one-byte character",
12354                        kw.to_ascii_lowercase()
12355                    ))
12356                };
12357                let mut chars = s.chars();
12358                let c = chars.next().ok_or_else(one_byte_err)?;
12359                if chars.next().is_some() || c.len_utf8() != 1 {
12360                    return Err(one_byte_err());
12361                }
12362                match kw.as_str() {
12363                    "DELIMITER" => opts.delimiter = Some(c),
12364                    "QUOTE" => opts.quote = Some(c),
12365                    _ => opts.escape = Some(c),
12366                }
12367            }
12368            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
12369            "FORCE_QUOTE" => {
12370                if matches!(self.peek(), Token::Star) {
12371                    self.advance();
12372                    opts.force_quote = Some(Vec::new());
12373                } else {
12374                    if !matches!(self.peek(), Token::LParen) {
12375                        return Err(self.err(alloc::format!(
12376                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
12377                            self.peek()
12378                        )));
12379                    }
12380                    self.advance();
12381                    let mut cols = Vec::new();
12382                    loop {
12383                        cols.push(self.expect_ident_like()?);
12384                        match self.peek() {
12385                            Token::Comma => {
12386                                self.advance();
12387                            }
12388                            Token::RParen => {
12389                                self.advance();
12390                                break;
12391                            }
12392                            other => {
12393                                return Err(self.err(alloc::format!(
12394                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
12395                                )));
12396                            }
12397                        }
12398                    }
12399                    opts.force_quote = Some(cols);
12400                }
12401            }
12402            "NULL" => {
12403                opts.null_str = Some(match self.advance() {
12404                    Token::String(s) => s,
12405                    other => {
12406                        return Err(self.err(alloc::format!(
12407                            "COPY NULL expects a quoted string, got {other:?}"
12408                        )));
12409                    }
12410                });
12411            }
12412            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
12413            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
12414            // FORCE_NULL too.
12415            "FORCE_NOT_NULL" | "FORCE_NULL" => {
12416                let cols = self.parse_copy_column_list(&kw)?;
12417                if kw == "FORCE_NOT_NULL" {
12418                    opts.force_not_null = Some(cols);
12419                } else {
12420                    opts.force_null = Some(cols);
12421                }
12422            }
12423            other => {
12424                // PG's wording, lowercased option name.
12425                return Err(self.err(alloc::format!(
12426                    "option \"{}\" not recognized",
12427                    other.to_ascii_lowercase()
12428                )));
12429            }
12430        }
12431        Ok(())
12432    }
12433
12434    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
12435    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
12436    /// is the `*` spelling.
12437    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
12438        if matches!(self.peek(), Token::Star) {
12439            self.advance();
12440            return Ok(Vec::new());
12441        }
12442        if !matches!(self.peek(), Token::LParen) {
12443            return Err(self.err(alloc::format!(
12444                "expected '(' or '*' after {kw}, got {:?}",
12445                self.peek()
12446            )));
12447        }
12448        self.advance();
12449        let mut cols = Vec::new();
12450        loop {
12451            cols.push(self.expect_ident_like()?);
12452            match self.peek() {
12453                Token::Comma => {
12454                    self.advance();
12455                }
12456                Token::RParen => {
12457                    self.advance();
12458                    break;
12459                }
12460                other => {
12461                    return Err(self.err(alloc::format!(
12462                        "expected ',' or ')' in {kw} list, got {other:?}"
12463                    )));
12464                }
12465            }
12466        }
12467        Ok(cols)
12468    }
12469
12470    fn parse_partition_bounds_tail(
12471        &mut self,
12472    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12473        use crate::ast::PartitionOfBoundsAst;
12474        match self.peek() {
12475            Token::Default => {
12476                self.advance();
12477                Ok(PartitionOfBoundsAst::Default)
12478            }
12479            Token::For => {
12480                self.advance();
12481                if !matches!(self.peek(), Token::Values) {
12482                    return Err(
12483                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12484                    );
12485                }
12486                self.advance();
12487                let want_with = matches!(
12488                    self.peek(),
12489                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12490                );
12491                if want_with {
12492                    self.advance();
12493                    if !matches!(self.peek(), Token::LParen) {
12494                        return Err(self.err(format!(
12495                            "expected '(' after FOR VALUES WITH, got {:?}",
12496                            self.peek()
12497                        )));
12498                    }
12499                    self.advance();
12500                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12501                    loop {
12502                        let key = self.expect_ident_like()?;
12503                        let n = match self.peek().clone() {
12504                            Token::Integer(v) if u32::try_from(v).is_ok() => {
12505                                self.advance();
12506                                v as u32
12507                            }
12508                            other => {
12509                                return Err(self.err(format!(
12510                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12511                                )));
12512                            }
12513                        };
12514                        match key.to_ascii_uppercase().as_str() {
12515                            "MODULUS" => modulus = Some(n),
12516                            "REMAINDER" => remainder = Some(n),
12517                            other => {
12518                                return Err(self.err(format!(
12519                                    "FOR VALUES WITH: unknown key {other:?}; \
12520                                     expected MODULUS or REMAINDER"
12521                                )));
12522                            }
12523                        }
12524                        match self.peek() {
12525                            Token::Comma => {
12526                                self.advance();
12527                            }
12528                            Token::RParen => {
12529                                self.advance();
12530                                break;
12531                            }
12532                            other => {
12533                                return Err(self.err(format!(
12534                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12535                                )));
12536                            }
12537                        }
12538                    }
12539                    let modulus = modulus
12540                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12541                    let remainder = remainder.ok_or_else(|| {
12542                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12543                    })?;
12544                    if modulus == 0 {
12545                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12546                    }
12547                    if remainder >= modulus {
12548                        return Err(self.err(format!(
12549                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12550                        )));
12551                    }
12552                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12553                }
12554                match self.peek() {
12555                    Token::From => {
12556                        self.advance();
12557                        let lower = Box::new(self.parse_partition_bound_expr()?);
12558                        if !matches!(self.peek(), Token::To) {
12559                            return Err(self.err(format!(
12560                                "expected TO after FROM (...), got {:?}",
12561                                self.peek()
12562                            )));
12563                        }
12564                        self.advance();
12565                        let upper = Box::new(self.parse_partition_bound_expr()?);
12566                        Ok(PartitionOfBoundsAst::Range { lower, upper })
12567                    }
12568                    Token::In => {
12569                        self.advance();
12570                        if !matches!(self.peek(), Token::LParen) {
12571                            return Err(self.err(format!(
12572                                "expected '(' after FOR VALUES IN, got {:?}",
12573                                self.peek()
12574                            )));
12575                        }
12576                        self.advance();
12577                        let mut values = Vec::new();
12578                        loop {
12579                            values.push(self.parse_expr(0)?);
12580                            match self.peek() {
12581                                Token::Comma => {
12582                                    self.advance();
12583                                }
12584                                Token::RParen => {
12585                                    self.advance();
12586                                    break;
12587                                }
12588                                other => {
12589                                    return Err(self.err(format!(
12590                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12591                                    )));
12592                                }
12593                            }
12594                        }
12595                        if values.is_empty() {
12596                            return Err(
12597                                self.err("FOR VALUES IN requires at least one literal".to_string())
12598                            );
12599                        }
12600                        Ok(PartitionOfBoundsAst::List { values })
12601                    }
12602                    other => Err(self.err(format!(
12603                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12604                    ))),
12605                }
12606            }
12607            other => Err(self.err(format!(
12608                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12609            ))),
12610        }
12611    }
12612
12613    /// v7.16.2 — peek for `information_schema.<tbl>` /
12614    /// `pg_catalog.<tbl>` triples and, if matched, consume all
12615    /// three tokens + return a synthetic table name the engine's
12616    /// SELECT path recognises as a virtual view. Returns `None`
12617    /// when the head doesn't look like a meta-qualified name.
12618    /// Used by `parse_table_ref` to bypass the
12619    /// `expect_ident_like` schema-strip for these specific PG
12620    /// meta schemas (mailrs round-10 A.3).
12621    fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12622        // Extract the schema name. Must be a plain ident token.
12623        let schema = match self.tokens.get(self.pos) {
12624            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12625            _ => return None,
12626        };
12627        // Dot.
12628        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12629            return None;
12630        }
12631        // The table-side ident may lex as a reserved keyword
12632        // (e.g. `Token::Tables`). Tolerate the common ones via a
12633        // helper that reads the trailing token's underlying name.
12634        let tbl = match self.tokens.get(self.pos + 2)? {
12635            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12636            Token::Tables => "tables".to_string(),
12637            // Other PG meta table names that may collide with
12638            // reserved keywords land here as needed.
12639            _ => return None,
12640        };
12641        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12642        // names so the synthetic name doesn't double-prefix
12643        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12644        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12645            ("__spg_info_", tbl.to_ascii_lowercase())
12646        } else if schema.eq_ignore_ascii_case("pg_catalog") {
12647            // v7.39 (round 541) — only the catalogs SPG actually
12648            // synthesises are rewritten, which is what the BARE path
12649            // has always checked. Anything else keeps its own name and
12650            // takes the ordinary route: `pg_stat_activity` and friends
12651            // resolve through meta_view_result, and a name that is no
12652            // catalog at all gets PG's "relation does not exist"
12653            // instead of a message about a view SPG cannot materialise.
12654            let lowered = tbl.to_ascii_lowercase();
12655            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12656                self.advance(); // schema
12657                self.advance(); // dot
12658                self.advance(); // tbl
12659                return Some((lowered.clone(), lowered));
12660            }
12661            let bare = lowered
12662                .strip_prefix("pg_")
12663                .map(alloc::string::String::from)
12664                .unwrap_or(lowered);
12665            ("__spg_pg_", bare)
12666        } else if schema.eq_ignore_ascii_case("mysql") {
12667            // v7.17.0 Phase 3.P0-65 — MySQL system schema
12668            // (`mysql.user`, `mysql.db`). Same synthetic-name
12669            // shape as pg_catalog.
12670            ("__spg_mysql_", tbl.to_ascii_lowercase())
12671        } else {
12672            return None;
12673        };
12674        self.advance(); // schema
12675        self.advance(); // dot
12676        self.advance(); // tbl
12677        Some((
12678            alloc::format!("{prefix}{normalised}"),
12679            tbl.to_ascii_lowercase(),
12680        ))
12681    }
12682
12683    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12684    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12685    /// implicit front of every search_path, so a bare reference to a
12686    /// known catalog table always means the catalog table. Only the
12687    /// names the engine actually synthesises are recognised — any
12688    /// other `pg_*` ident stays a user table (mailrs embed round-12).
12689    fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12690        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12691        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12692        // `pg_catalog` at the front of every search_path. (pg_stat_activity
12693        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12694        // through the meta_view_result path instead, and already resolve
12695        // bare — they must NOT be listed here or the __spg_ rewrite would
12696        // mis-target them.)
12697        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12698        let name = match self.tokens.get(self.pos) {
12699            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12700            _ => return None,
12701        };
12702        // A following dot means this ident is a schema qualifier,
12703        // not a table name — let the qualified path handle it.
12704        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12705            return None;
12706        }
12707        if !PG_META_TABLES.contains(&name.as_str()) {
12708            return None;
12709        }
12710        self.advance();
12711        let bare = name.strip_prefix("pg_").unwrap_or(&name);
12712        Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12713    }
12714
12715    /// Consume a bare ident if its lowercase matches `kw`, else err.
12716    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12717    /// Peeks only; the caller advances.
12718    fn peek_keyword_ident(&self, kw: &str) -> bool {
12719        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12720    }
12721
12722    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12723        match self.advance() {
12724            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12725            other => Err(ParseError {
12726                message: format!("expected {kw:?}, got {other:?}"),
12727                token_pos: self.consumed_pos(),
12728            }),
12729        }
12730    }
12731
12732    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12733    /// literal (`'foo'`) — same shape used by CREATE USER for the
12734    /// username slot.
12735    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12736        match self.advance() {
12737            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12738            other => Err(ParseError {
12739                message: format!("expected identifier or string, got {other:?}"),
12740                token_pos: self.consumed_pos(),
12741            }),
12742        }
12743    }
12744
12745    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12746        match self.advance() {
12747            Token::String(s) => Ok(s),
12748            other => Err(ParseError {
12749                message: format!("expected quoted string, got {other:?}"),
12750                token_pos: self.consumed_pos(),
12751            }),
12752        }
12753    }
12754
12755    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12756        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12757        // subqueries recurse through here without passing
12758        // parse_expr; share the same nesting budget.
12759        self.enter_nested()?;
12760        let r = self.parse_select_stmt_inner();
12761        self.nest_depth -= 1;
12762        r
12763    }
12764
12765    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12766        // Caller dispatches on Token::Select; the inner helper handles
12767        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12768        // get a fresh bare-select parse and may not have their own ORDER
12769        // BY / LIMIT.
12770        let mut head = self.parse_bare_select()?;
12771        let into = self.pending_select_into.take();
12772        self.parse_setop_chain_into(&mut head)?;
12773        self.parse_select_tail_into(&mut head)?;
12774        // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12775        // `CREATE TABLE t AS SELECT …`, which is what a comment in
12776        // `ast.rs` has claimed since v7.38 and what only CTAS actually
12777        // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12778        // to the body, as it does in PostgreSQL.
12779        if let Some((name, temporary)) = into {
12780            return Ok(Statement::CreateMaterializedView(
12781                crate::ast::CreateMaterializedViewStatement {
12782                    temporary,
12783                    name,
12784                    if_not_exists: false,
12785                    columns: Vec::new(),
12786                    body: head,
12787                    with_data: true,
12788                    as_plain_table: true,
12789                },
12790            ));
12791        }
12792        Ok(Statement::Select(head))
12793    }
12794
12795    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12796    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12797    /// token), and INTERSECT [ALL] (a bare ident — it was never
12798    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12799    /// tighter than UNION / EXCEPT — the executor folds the chain
12800    /// left-to-right, which is already correct for LEADING
12801    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12802    /// pair nests into that previous peer, so A UNION B INTERSECT C
12803    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12804    /// groups.
12805    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12806        // A parenthesized group arrives with its own (already
12807        // regrouped) unions on `head`; only the pairs THIS chain
12808        // appends participate in the precedence regroup below —
12809        // nesting an outer INTERSECT into a group-internal peer
12810        // would dissolve the explicit grouping.
12811        let boundary = head.unions.len();
12812        loop {
12813            let base = match self.peek() {
12814                Token::Union => UnionKind::Distinct,
12815                Token::Except => UnionKind::Except,
12816                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12817                _ => break,
12818            };
12819            self.advance();
12820            let kind = if matches!(self.peek(), Token::All) {
12821                self.advance();
12822                match base {
12823                    UnionKind::Distinct => UnionKind::All,
12824                    UnionKind::Except => UnionKind::ExceptAll,
12825                    _ => UnionKind::IntersectAll,
12826                }
12827            } else {
12828                base
12829            };
12830            let peer = self.parse_bare_select()?;
12831            head.unions.push((kind, peer));
12832        }
12833        let mut pairs = core::mem::take(&mut head.unions);
12834        let tail = pairs.split_off(boundary);
12835        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12836        for (kind, peer) in tail {
12837            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12838            // An intersect nests into the previous element of THIS
12839            // chain only; with no new previous element it stays at
12840            // the outer level (the left fold applies it to the
12841            // whole head, group included).
12842            match (
12843                is_intersect,
12844                regrouped.len() > boundary,
12845                regrouped.last_mut(),
12846            ) {
12847                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12848                _ => regrouped.push((kind, peer)),
12849            }
12850        }
12851        head.unions = regrouped;
12852        Ok(())
12853    }
12854
12855    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12856    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12857    /// the top-level bare VALUES statement reuses it verbatim.
12858    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12859    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12860    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12861    /// where the grouping-set universe is still in scope.
12862    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12863        if !matches!(self.peek(), Token::Order) {
12864            return Ok(Vec::new());
12865        }
12866        self.advance();
12867        if !self.peek_is_by() {
12868            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12869        }
12870        self.advance();
12871        let mut keys = Vec::new();
12872        loop {
12873            // v7.39 (round 691) — save/restore, the discipline this parser
12874            // already uses around `pending_sample_preds`, so a subquery inside
12875            // a key neither inherits nor leaks the channel.
12876            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12877            let saved_coll = self.order_key_collation.take();
12878            let parsed = self.parse_expr(0);
12879            self.in_order_by_key = saved_flag;
12880            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12881            let expr = parsed?;
12882            let desc = if matches!(self.peek(), Token::Desc) {
12883                self.advance();
12884                true
12885            } else if matches!(self.peek(), Token::Asc) {
12886                self.advance();
12887                false
12888            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12889                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12890                // one ordering per type, so the btree comparison operators map
12891                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12892                // would need a custom operator class — honest error.
12893                self.advance();
12894                match self.advance() {
12895                    Token::Lt | Token::LtEq => false,
12896                    Token::Gt | Token::GtEq => true,
12897                    other => {
12898                        return Err(self.err(alloc::format!(
12899                            "ORDER BY USING supports the btree comparison \
12900                             operators (< <= > >=); got {other:?}"
12901                        )));
12902                    }
12903                }
12904            } else {
12905                false
12906            };
12907            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12908            let nulls_first = self.parse_optional_nulls_placement()?;
12909            keys.push(OrderBy {
12910                expr,
12911                desc,
12912                nulls_first,
12913                collation,
12914            });
12915            if matches!(self.peek(), Token::Comma) {
12916                self.advance();
12917            } else {
12918                break;
12919            }
12920        }
12921        Ok(keys)
12922    }
12923
12924    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12925        // v7.39 (round 135) — a grouping-set query may have already parsed +
12926        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12927        // no ORDER BY token is present, keep that pre-set order_by rather than
12928        // clobbering it with an empty list.
12929        let parsed_keys = self.parse_order_by_keys()?;
12930        head.order_by = if parsed_keys.is_empty() {
12931            core::mem::take(&mut head.order_by)
12932        } else {
12933            parsed_keys
12934        };
12935        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12936        // order. PG's grammar takes a limit clause and an offset clause
12937        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12938        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12939        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12940        // spelling died on `expected end of input, got Limit`.
12941        //
12942        // Each may appear at most once, and LIMIT and FETCH FIRST are
12943        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12944        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12945        // A second one is left unconsumed here, which the caller reports
12946        // as trailing input rather than silently taking the last.
12947        let mut saw_limit = false;
12948        let mut saw_offset = false;
12949        loop {
12950            if !saw_limit && matches!(self.peek(), Token::Limit) {
12951                self.advance();
12952                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12953                // PG synonyms for "no limit". Treat both as None
12954                // (no head.limit set) so the engine's existing
12955                // unlimited-result path takes over. Reject was the
12956                // pre-5.1 behaviour and broke pg_dump-flavoured
12957                // tooling that occasionally emits LIMIT NULL.
12958                if self.consume_limit_unbounded_sentinel() {
12959                    head.limit = None;
12960                } else {
12961                    let first = self.parse_limit_expr("LIMIT")?;
12962                    // MySQL `LIMIT offset, count` — the first number is
12963                    // the offset when a comma follows.
12964                    if matches!(self.peek(), Token::Comma) {
12965                        self.advance();
12966                        let count = self.parse_limit_expr("LIMIT")?;
12967                        head.offset = Some(first);
12968                        saw_offset = true;
12969                        head.limit = Some(count);
12970                    } else {
12971                        head.limit = Some(first);
12972                    }
12973                }
12974                saw_limit = true;
12975                continue;
12976            }
12977            if !saw_offset && matches!(self.peek(), Token::Offset) {
12978                self.advance();
12979                // PG also accepts an optional `ROW` / `ROWS` trailer
12980                // after the offset value (`OFFSET 10 ROWS`). The
12981                // FETCH-FIRST branch below relies on the same.
12982                let off = self.parse_limit_expr("OFFSET")?;
12983                self.consume_optional_rows_keyword();
12984                head.offset = Some(off);
12985                saw_offset = true;
12986                continue;
12987            }
12988            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12989            // the SQL-standard alias for LIMIT. PG accepts both
12990            // spellings interchangeably; pg_dump emits FETCH FIRST in
12991            // newer versions. We map it onto `head.limit` so the
12992            // engine path is unified.
12993            if !saw_limit
12994                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12995                    if s.eq_ignore_ascii_case("fetch"))
12996            {
12997                self.advance(); // FETCH
12998                // `FIRST` or `NEXT` (both legal per SQL standard).
12999                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13000                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
13001                {
13002                    self.advance();
13003                }
13004                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
13005                // implicit 1 — but we always consume one if present).
13006                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13007                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
13008                {
13009                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
13010                    crate::ast::LimitExpr::Literal(1)
13011                } else {
13012                    self.parse_limit_expr("FETCH FIRST")?
13013                };
13014                // Eat `ROW` / `ROWS` if not already consumed above.
13015                self.consume_optional_rows_keyword();
13016                // Optional `ONLY` (the spec form) — or the SQL:2008
13017                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
13018                // now honours WITH TIES by extending past the LIMIT
13019                // truncation point through every row that shares the
13020                // last-kept row's ORDER BY key.
13021                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13022                    if s.eq_ignore_ascii_case("only"))
13023                {
13024                    self.advance();
13025                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13026                    if s.eq_ignore_ascii_case("with"))
13027                {
13028                    self.advance(); // WITH
13029                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13030                        if s.eq_ignore_ascii_case("ties"))
13031                    {
13032                        self.advance();
13033                        head.limit_with_ties = true;
13034                    }
13035                }
13036                head.limit = Some(count);
13037                saw_limit = true;
13038                continue;
13039            }
13040            break;
13041        }
13042        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
13043        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
13044        //       [ OF table_name [, …] ]
13045        //       [ NOWAIT | SKIP LOCKED ]
13046        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
13047        // FOR SHARE OF t2`). SPG is a single-writer engine — every
13048        // SELECT already returns a consistent snapshot — so these
13049        // are accept-and-discard: the parser absorbs them so
13050        // mailrs / Rails / Django code paths that emit `SELECT
13051        // … FOR UPDATE` for advisory pessimistic locking load
13052        // without a parser error. The on-disk locking model is
13053        // unchanged; callers that rely on FOR UPDATE for read-
13054        // through-write ordering still get the right answer
13055        // because SPG serialises writes anyway.
13056        head.locking = self
13057            .consume_optional_for_lock_clauses()
13058            .map(alloc::boxed::Box::new);
13059        Ok(())
13060    }
13061
13062    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
13063    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
13064    /// LOCKED ]` trailers. Each clause is fully accepted and
13065    /// discarded — SPG's single-writer model already satisfies the
13066    /// callers' implicit ordering requirement. Stops at the first
13067    /// token that isn't `FOR`.
13068    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
13069        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
13070        // not discarded. PG keeps the strongest of several clauses; the
13071        // policy of the last one wins, which is what this loop records.
13072        let mut seen: Option<crate::ast::LockingClause> = None;
13073        while matches!(self.peek(), Token::For) {
13074            // v7.37.14 (A2.5-stub) — record that this query asked
13075            // for a row lock the parser is about to silently
13076            // discard. Operators surface the count via
13077            // `spg_sql::silent_for_update_count()` so they can
13078            // gauge how much of the workload depends on advisory
13079            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
13080            // before v7.37.15's per-row tuple locking lands.
13081            crate::record_silent_for_update_clause();
13082            self.advance(); // FOR
13083            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
13084            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
13085            let mut no_key = false;
13086            let mut key = false;
13087            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13088                if s.eq_ignore_ascii_case("no"))
13089            {
13090                self.advance(); // NO
13091                no_key = true;
13092                // The next ident should be KEY but be generous;
13093                // anything followed by UPDATE/SHARE is accepted.
13094                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13095                    if s.eq_ignore_ascii_case("key"))
13096                {
13097                    self.advance(); // KEY
13098                }
13099            }
13100            // `KEY` prefix (PG `FOR KEY SHARE`).
13101            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13102                if s.eq_ignore_ascii_case("key"))
13103            {
13104                self.advance(); // KEY
13105                key = true;
13106            }
13107            // Lock-strength keyword: UPDATE / SHARE. Required, but
13108            // we're lenient — an unexpected token here just bails
13109            // (we already consumed FOR; caller's downstream
13110            // dispatch will error if anything actually depends on
13111            // the trailing tokens).
13112            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13113                if s.eq_ignore_ascii_case("update"));
13114            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13115                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
13116            {
13117                self.advance();
13118                use crate::ast::LockStrength as LS;
13119                let strength = match (is_update, no_key, key) {
13120                    (true, true, _) => LS::NoKeyUpdate,
13121                    (true, _, _) => LS::Update,
13122                    (false, _, true) => LS::KeyShare,
13123                    (false, _, _) => LS::Share,
13124                };
13125                seen = Some(crate::ast::LockingClause {
13126                    strength,
13127                    of_tables: alloc::vec::Vec::new(),
13128                    policy: crate::ast::LockWait::Wait,
13129                });
13130            } else {
13131                // FOR by itself (or `FOR KEY` with nothing after) —
13132                // give up on the lock-clause path. We've already
13133                // advanced past FOR; further attempts to parse
13134                // here would clobber state.
13135                return seen;
13136            }
13137            // Optional `OF tbl[, tbl …]`. mailrs emits this when
13138            // joining and locking only a subset of tables.
13139            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13140                if s.eq_ignore_ascii_case("of"))
13141            {
13142                self.advance(); // OF
13143                #[allow(clippy::while_let_loop)]
13144                loop {
13145                    match self.peek() {
13146                        Token::Ident(_) | Token::QuotedIdent(_) => {
13147                            // v7.39 (round 294) — the name is CAPTURED now: PG
13148                            // validates it against the FROM clause, and an
13149                            // uncaptured list silently means "lock everything".
13150                            let mut nm = match self.advance() {
13151                                Token::Ident(n) | Token::QuotedIdent(n) => n,
13152                                _ => alloc::string::String::new(),
13153                            };
13154                            // Optional schema-qualified `schema.table`.
13155                            if matches!(self.peek(), Token::Dot) {
13156                                self.advance();
13157                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
13158                                {
13159                                    self.advance();
13160                                    nm = n;
13161                                }
13162                            }
13163                            if let Some(c) = seen.as_mut() {
13164                                c.of_tables.push(nm);
13165                            }
13166                        }
13167                        _ => break,
13168                    }
13169                    if matches!(self.peek(), Token::Comma) {
13170                        self.advance();
13171                    } else {
13172                        break;
13173                    }
13174                }
13175            }
13176            // Optional `NOWAIT` | `SKIP LOCKED`.
13177            match self.peek().clone() {
13178                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
13179                    self.advance();
13180                    if let Some(c) = seen.as_mut() {
13181                        c.policy = crate::ast::LockWait::NoWait;
13182                    }
13183                }
13184                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
13185                    self.advance(); // SKIP
13186                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13187                        if s.eq_ignore_ascii_case("locked"))
13188                    {
13189                        self.advance(); // LOCKED
13190                        if let Some(c) = seen.as_mut() {
13191                            c.policy = crate::ast::LockWait::SkipLocked;
13192                        }
13193                    }
13194                }
13195                _ => {}
13196            }
13197            // Loop: PG allows multiple FOR clauses chained.
13198        }
13199        seen
13200    }
13201
13202    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
13203    /// Bind value gets resolved during prepared-statement Execute;
13204    /// the Pratt expression parser would over-accept here (e.g.
13205    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
13206    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
13207    /// sentinel tokens (PG synonyms for "no limit"). Returns true
13208    /// when one was consumed; caller skips the regular
13209    /// limit-value parse and leaves `head.limit` at None.
13210    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
13211        if matches!(self.peek(), Token::Null) {
13212            self.advance();
13213            return true;
13214        }
13215        if matches!(self.peek(), Token::All) {
13216            self.advance();
13217            return true;
13218        }
13219        false
13220    }
13221
13222    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
13223    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
13224    /// SQL-standard shape. No-op when missing.
13225    fn consume_optional_rows_keyword(&mut self) {
13226        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13227            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
13228        {
13229            self.advance();
13230        }
13231    }
13232
13233    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
13234    ///
13235    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
13236    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
13237    /// constant, which is why that spelling keeps the token path below.
13238    ///
13239    /// Constants are folded here rather than carried into the tree: the
13240    /// 15+ execution paths that read the row count go through
13241    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
13242    /// means "no limit". A clause the engine could not resolve would
13243    /// therefore return the WHOLE table instead of failing. Folding at
13244    /// parse time keeps that impossible; a non-constant clause is still
13245    /// a clean error (recorded residual — closing it wants a resolution
13246    /// pre-pass on the simple-query path, where `substitute_placeholders`
13247    /// does not run).
13248    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13249        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
13250        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
13251        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
13252        // ONLY` both work (its grammar takes a c_expr). Both measured
13253        // against PG 18.4 in round 305.
13254        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
13255            return self.parse_limit_constant(label);
13256        }
13257        // One pass, no rewind: `advance()` takes each token by
13258        // `mem::replace`, so a consumed token reads back as Eof and this
13259        // parser cannot backtrack. Everything — bare literal included —
13260        // is therefore folded from the parsed expression rather than
13261        // re-read from the token stream.
13262        let start = self.pos;
13263        let e = self.parse_expr(0)?;
13264        if let crate::ast::Expr::Placeholder(n) = e {
13265            return Ok(crate::ast::LimitExpr::Placeholder(n));
13266        }
13267        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13268        match fold_limit_constant(&e) {
13269            Some(Ok(v)) if v < 0 => Err(ParseError {
13270                message: alloc::format!("{neg_label} must not be negative"),
13271                token_pos: start,
13272            }),
13273            Some(Ok(v)) => u32::try_from(v)
13274                .map(crate::ast::LimitExpr::Literal)
13275                .map_err(|_| ParseError {
13276                    message: alloc::format!("{label} value too large: {v}"),
13277                    token_pos: start,
13278                }),
13279            Some(Err(message)) => Err(ParseError {
13280                message: message.replace("{L}", neg_label),
13281                token_pos: start,
13282            }),
13283            // v7.39 (round 305, V23) — not foldable at parse time
13284            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
13285            // expression; the engine evaluates it once before dispatch.
13286            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
13287        }
13288    }
13289
13290    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13291        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
13292        // coercion rules, not just an integer token: a NUMERIC rounds half
13293        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
13294        // refused with PG's wording ("LIMIT must not be negative", 2201W /
13295        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
13296        // content, failing as an input-syntax error on the value. General
13297        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
13298        // they need an Expr-carrying LimitExpr variant.
13299        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13300        let err_at = |message: alloc::string::String, pos: usize| ParseError {
13301            message,
13302            token_pos: pos,
13303        };
13304        match self.advance() {
13305            Token::Integer(n) if n >= 0 => u32::try_from(n)
13306                .map(crate::ast::LimitExpr::Literal)
13307                .map_err(|_| ParseError {
13308                    message: alloc::format!("{label} value too large: {n}"),
13309                    token_pos: self.consumed_pos(),
13310                }),
13311            Token::Integer(_) => Err(err_at(
13312                alloc::format!("{neg_label} must not be negative"),
13313                self.pos.saturating_sub(1),
13314            )),
13315            Token::Numeric(t) => {
13316                let pos = self.pos.saturating_sub(1);
13317                let v: f64 = t.parse().map_err(|_| {
13318                    err_at(
13319                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13320                        pos,
13321                    )
13322                })?;
13323                if v < 0.0 {
13324                    return Err(err_at(
13325                        alloc::format!("{neg_label} must not be negative"),
13326                        pos,
13327                    ));
13328                }
13329                // Round half away from zero — PG's numeric→bigint cast.
13330                // (no_std: no f64::round; v is non-negative, so truncating
13331                // v + 0.5 is the same thing.)
13332                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
13333                let rounded = (v + 0.5) as u64;
13334                u32::try_from(rounded)
13335                    .map(crate::ast::LimitExpr::Literal)
13336                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
13337            }
13338            Token::Minus => {
13339                let pos = self.pos.saturating_sub(1);
13340                match self.peek() {
13341                    Token::Integer(_) | Token::Numeric(_) => {
13342                        self.advance();
13343                        Err(err_at(
13344                            alloc::format!("{neg_label} must not be negative"),
13345                            pos,
13346                        ))
13347                    }
13348                    other => Err(err_at(
13349                        alloc::format!(
13350                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13351                        ),
13352                        pos,
13353                    )),
13354                }
13355            }
13356            Token::String(t) => {
13357                let pos = self.pos.saturating_sub(1);
13358                match t.trim().parse::<i64>() {
13359                    Ok(n) if n < 0 => Err(err_at(
13360                        alloc::format!("{neg_label} must not be negative"),
13361                        pos,
13362                    )),
13363                    Ok(n) => u32::try_from(n)
13364                        .map(crate::ast::LimitExpr::Literal)
13365                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
13366                    Err(_) => Err(err_at(
13367                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13368                        pos,
13369                    )),
13370                }
13371            }
13372            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
13373            other => Err(ParseError {
13374                message: alloc::format!(
13375                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13376                ),
13377                token_pos: self.consumed_pos(),
13378            }),
13379        }
13380    }
13381
13382    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
13383    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
13384    /// `unions` empty and `order_by` / `limit` `None`; the top-level
13385    /// `parse_select_stmt` is responsible for filling those in.
13386    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
13387    /// call in the expression tree to the per-set integer bitmask
13388    /// (PG semantics: one bit per argument, MSB first; 1 = the key
13389    /// is dropped in this grouping set). Runs during the ROLLUP /
13390    /// CUBE / GROUPING SETS expansion, where the set is known.
13391    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
13392    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
13393    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
13394        if let Expr::FunctionCall { name, .. } = expr
13395            && name.eq_ignore_ascii_case("grouping")
13396        {
13397            if !out.iter().any(|e| e == expr) {
13398                out.push(expr.clone());
13399            }
13400            return;
13401        }
13402        match expr {
13403            Expr::Binary { lhs, rhs, .. } => {
13404                Self::collect_grouping_calls(lhs, out);
13405                Self::collect_grouping_calls(rhs, out);
13406            }
13407            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13408                Self::collect_grouping_calls(expr, out)
13409            }
13410            Expr::FunctionCall { args, .. } => {
13411                for a in args {
13412                    Self::collect_grouping_calls(a, out);
13413                }
13414            }
13415            Expr::Case {
13416                operand,
13417                branches,
13418                else_branch,
13419            } => {
13420                if let Some(o) = operand {
13421                    Self::collect_grouping_calls(o, out);
13422                }
13423                for (c, v) in branches {
13424                    Self::collect_grouping_calls(c, out);
13425                    Self::collect_grouping_calls(v, out);
13426                }
13427                if let Some(x) = else_branch {
13428                    Self::collect_grouping_calls(x, out);
13429                }
13430            }
13431            _ => {}
13432        }
13433    }
13434
13435    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
13436    /// `grp_exprs[k]` with a reference to the synthetic ordering column
13437    /// `__grp_ord_k` (injected per grouping-set branch).
13438    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
13439        if let Expr::FunctionCall { name, .. } = expr
13440            && name.eq_ignore_ascii_case("grouping")
13441        {
13442            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13443                *expr = Expr::Column(crate::ast::ColumnName {
13444                    qualifier: None,
13445                    name: alloc::format!("__grp_ord_{k}"),
13446                });
13447            }
13448            return;
13449        }
13450        match expr {
13451            Expr::Binary { lhs, rhs, .. } => {
13452                Self::rewrite_grouping_to_col(lhs, grp_exprs);
13453                Self::rewrite_grouping_to_col(rhs, grp_exprs);
13454            }
13455            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13456                Self::rewrite_grouping_to_col(expr, grp_exprs)
13457            }
13458            Expr::FunctionCall { args, .. } => {
13459                for a in args {
13460                    Self::rewrite_grouping_to_col(a, grp_exprs);
13461                }
13462            }
13463            Expr::Case {
13464                operand,
13465                branches,
13466                else_branch,
13467            } => {
13468                if let Some(o) = operand {
13469                    Self::rewrite_grouping_to_col(o, grp_exprs);
13470                }
13471                for (c, v) in branches {
13472                    Self::rewrite_grouping_to_col(c, grp_exprs);
13473                    Self::rewrite_grouping_to_col(v, grp_exprs);
13474                }
13475                if let Some(x) = else_branch {
13476                    Self::rewrite_grouping_to_col(x, grp_exprs);
13477                }
13478            }
13479            _ => {}
13480        }
13481    }
13482
13483    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13484    /// as the list of key sets it contributes. A bare expression is one
13485    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13486    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13487    /// the concatenation of its items' sets, where an item is itself an
13488    /// element, a parenthesized key list, or the empty set `()`. A
13489    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13490    /// move together.
13491    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13492        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13493        // ROLLUP ( … ) / CUBE ( … )
13494        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13495            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13496        {
13497            let is_cube = is_kw(self.peek(), "cube");
13498            self.advance(); // ROLLUP / CUBE
13499            self.advance(); // (
13500            let mut units: Vec<Vec<Expr>> = Vec::new();
13501            loop {
13502                if matches!(self.peek(), Token::LParen) {
13503                    // Composite unit: (a, b) rolls up as one.
13504                    self.advance();
13505                    let mut unit = Vec::new();
13506                    if !matches!(self.peek(), Token::RParen) {
13507                        loop {
13508                            unit.push(self.parse_expr(0)?);
13509                            match self.peek() {
13510                                Token::Comma => {
13511                                    self.advance();
13512                                }
13513                                Token::RParen => break,
13514                                other => {
13515                                    return Err(self.err(format!(
13516                                        "expected ',' or ')' in grouping unit, got {other:?}"
13517                                    )));
13518                                }
13519                            }
13520                        }
13521                    }
13522                    self.advance(); // )
13523                    units.push(unit);
13524                } else {
13525                    units.push(alloc::vec![self.parse_expr(0)?]);
13526                }
13527                match self.peek() {
13528                    Token::Comma => {
13529                        self.advance();
13530                    }
13531                    Token::RParen => break,
13532                    other => {
13533                        return Err(self.err(format!(
13534                            "expected ',' or ')' in grouping list, got {other:?}"
13535                        )));
13536                    }
13537                }
13538            }
13539            self.advance(); // )
13540            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13541                units
13542                    .iter()
13543                    .zip(unit_sel.iter())
13544                    .filter(|(_, keep)| **keep)
13545                    .flat_map(|(u, _)| u.iter().cloned())
13546                    .collect()
13547            };
13548            let n = units.len();
13549            if is_cube {
13550                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13551                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13552                    .collect();
13553                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13554                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13555            }
13556            return Ok((0..=n)
13557                .rev()
13558                .map(|keep| {
13559                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13560                    flatten(&sel)
13561                })
13562                .collect());
13563        }
13564        // GROUPING SETS ( item [, item]* )
13565        if is_kw(self.peek(), "grouping")
13566            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13567        {
13568            self.advance(); // GROUPING
13569            self.advance(); // SETS
13570            if !matches!(self.peek(), Token::LParen) {
13571                return Err(self.err(format!(
13572                    "expected '(' after GROUPING SETS, got {:?}",
13573                    self.peek()
13574                )));
13575            }
13576            self.advance(); // outer (
13577            let mut sets: Vec<Vec<Expr>> = Vec::new();
13578            loop {
13579                if matches!(self.peek(), Token::LParen) {
13580                    // A parenthesized key list (or the empty set).
13581                    self.advance();
13582                    let mut set = Vec::new();
13583                    if !matches!(self.peek(), Token::RParen) {
13584                        loop {
13585                            set.push(self.parse_expr(0)?);
13586                            match self.peek() {
13587                                Token::Comma => {
13588                                    self.advance();
13589                                }
13590                                Token::RParen => break,
13591                                other => {
13592                                    return Err(self.err(format!(
13593                                        "expected ',' or ')' in grouping set, got {other:?}"
13594                                    )));
13595                                }
13596                            }
13597                        }
13598                    }
13599                    self.advance(); // )
13600                    sets.push(set);
13601                } else {
13602                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13603                    // bare expression.
13604                    sets.extend(self.parse_grouping_element()?);
13605                }
13606                match self.peek() {
13607                    Token::Comma => {
13608                        self.advance();
13609                    }
13610                    Token::RParen => break,
13611                    other => {
13612                        return Err(self.err(format!(
13613                            "expected ',' or ')' after a grouping set, got {other:?}"
13614                        )));
13615                    }
13616                }
13617            }
13618            self.advance(); // outer )
13619            return Ok(sets);
13620        }
13621        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13622    }
13623
13624    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13625        // v7.38 (read01) — a reference to a key that is dropped in this grouping
13626        // set evaluates to NULL, at any depth. Previously only a *top-level*
13627        // select item equal to a dropped key was nullified, so a key nested in
13628        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13629        // column and failed to resolve against the set's synthetic schema.
13630        if dropped.iter().any(|d| d == expr) {
13631            *expr = Expr::Literal(Literal::Null);
13632            return;
13633        }
13634        if let Expr::FunctionCall { name, args } = expr
13635            && name.eq_ignore_ascii_case("grouping")
13636        {
13637            let mut mask: i64 = 0;
13638            for a in args.iter() {
13639                mask <<= 1;
13640                if dropped.iter().any(|d| d == a) {
13641                    mask |= 1;
13642                }
13643            }
13644            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13645            // literal: a bare integer in a select item is indistinguishable
13646            // from a positional reference once `ORDER BY 1` substitutes the
13647            // item back in, and the round-232 position check then read the
13648            // mask value as an out-of-range position. The cast changes
13649            // nothing semantically (grouping() is integer).
13650            *expr = Expr::Cast {
13651                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13652                target: crate::ast::CastTarget::Int,
13653            };
13654            return;
13655        }
13656        // Generic recursion over the common expression shapes the
13657        // SELECT list uses; anything without child expressions is
13658        // left alone.
13659        match expr {
13660            // v7.40.0 — an AGGREGATE's argument is NOT nullified.
13661            //
13662            // A grouping column is NULL in the OUTPUT of a set that
13663            // drops it, and an aggregate over it still aggregates the
13664            // real values. Measured, over 0,1,2,3:
13665            //
13666            // ```text
13667            //   SELECT qty, SUM(qty) … GROUP BY ROLLUP(qty)
13668            //     PostgreSQL 18.6   the total row is  NULL | 6
13669            //     MySQL 9.7.2       the total row is  NULL | 6
13670            //     SPG 7.39.13                         NULL | NULL
13671            // ```
13672            //
13673            // The round that made this walk descend "at any depth" was
13674            // right about `COALESCE(g,'TOTAL')` and wrong about
13675            // `SUM(g)`: it turned the aggregate's own input into a NULL
13676            // literal, so the grand total of a rollup keyed on the
13677            // summed column answered nothing. Wrong on BOTH faces.
13678            //
13679            // `grouping(…)` is settled above, before this, so it keeps
13680            // reading the dropped set.
13681            Expr::FunctionCall { name, args } => {
13682                if is_aggregate_function_name(name) {
13683                    return;
13684                }
13685                for a in args {
13686                    Self::substitute_grouping_calls(a, dropped);
13687                }
13688            }
13689            Expr::AggregateOrdered { .. } => {}
13690            Expr::Binary { lhs, rhs, .. } => {
13691                Self::substitute_grouping_calls(lhs, dropped);
13692                Self::substitute_grouping_calls(rhs, dropped);
13693            }
13694            Expr::Unary { expr: inner, .. } => {
13695                Self::substitute_grouping_calls(inner, dropped);
13696            }
13697            Expr::Cast { expr: inner, .. } => {
13698                Self::substitute_grouping_calls(inner, dropped);
13699            }
13700            Expr::Case {
13701                operand,
13702                branches,
13703                else_branch,
13704            } => {
13705                if let Some(op) = operand {
13706                    Self::substitute_grouping_calls(op, dropped);
13707                }
13708                for (w, t) in branches {
13709                    Self::substitute_grouping_calls(w, dropped);
13710                    Self::substitute_grouping_calls(t, dropped);
13711                }
13712                if let Some(e) = else_branch {
13713                    Self::substitute_grouping_calls(e, dropped);
13714                }
13715            }
13716            // v7.38 (read01) — recurse into the remaining child-bearing shapes
13717            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13718            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13719            // …` is the canonical rollup-total label idiom).
13720            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13721            Expr::Like { expr, pattern, .. } => {
13722                Self::substitute_grouping_calls(expr, dropped);
13723                Self::substitute_grouping_calls(pattern, dropped);
13724            }
13725            Expr::InList { expr, list, .. } => {
13726                Self::substitute_grouping_calls(expr, dropped);
13727                for item in list {
13728                    Self::substitute_grouping_calls(item, dropped);
13729                }
13730            }
13731            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13732            Expr::Array(items) => {
13733                for item in items {
13734                    Self::substitute_grouping_calls(item, dropped);
13735                }
13736            }
13737            Expr::ArraySubscript { target, index } => {
13738                Self::substitute_grouping_calls(target, dropped);
13739                Self::substitute_grouping_calls(index, dropped);
13740            }
13741            Expr::ArraySlice { target, lo, hi } => {
13742                Self::substitute_grouping_calls(target, dropped);
13743                if let Some(lo) = lo {
13744                    Self::substitute_grouping_calls(lo, dropped);
13745                }
13746                if let Some(hi) = hi {
13747                    Self::substitute_grouping_calls(hi, dropped);
13748                }
13749            }
13750            Expr::AnyAll { expr, array, .. } => {
13751                Self::substitute_grouping_calls(expr, dropped);
13752                Self::substitute_grouping_calls(array, dropped);
13753            }
13754            _ => {}
13755        }
13756    }
13757
13758    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13759        // v7.40.11 — an UNPARENTHESISED `VALUES` list is a query block
13760        // too, so it can be the PEER of a set operation:
13761        //
13762        //   SELECT 1 UNION ALL VALUES (2)
13763        //
13764        // The parenthesised form has been a peer since v7.37 D.20 and
13765        // the CTE-body form since 17.6; these two unbracketed positions
13766        // were the pair nobody wrote a case for. Modern psql builds its
13767        // describe queries with `UNION ALL VALUES`, so every backslash
13768        // command — `\d`, `\dt`, `\di` — failed with a syntax error
13769        // pointing into a query the user did not write.
13770        if matches!(self.peek(), Token::Values) {
13771            self.advance(); // VALUES
13772            return self.parse_values_rows_body();
13773        }
13774        // v7.37.17 (17.6 siblings) — parenthesized set-operation
13775        // group: `( <select chain> )` usable anywhere a query block
13776        // is (head or peer of an outer chain). The group's own
13777        // unions ride the returned SelectStatement; the executor's
13778        // nested-peer recursion runs them.
13779        if matches!(self.peek(), Token::LParen)
13780            && matches!(
13781                self.tokens.get(self.pos + 1),
13782                Some(Token::Select | Token::LParen | Token::Values)
13783            )
13784        {
13785            self.advance(); // (
13786            self.enter_nested()?;
13787            // v7.37 D.20 — a group whose head is a VALUES list:
13788            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13789            // otherwise recurse into a nested SELECT/group head.
13790            let mut head = (if matches!(self.peek(), Token::Values) {
13791                self.advance(); // VALUES
13792                self.parse_values_rows_body()
13793            } else {
13794                self.parse_bare_select()
13795            })
13796            .and_then(|mut h| {
13797                self.parse_setop_chain_into(&mut h)?;
13798                Ok(h)
13799            });
13800            self.nest_depth -= 1;
13801            let mut head = match &mut head {
13802                Ok(h) => core::mem::take(h),
13803                Err(_) => return head,
13804            };
13805            // v7.37.17 (17.6 siblings) — group-internal tail:
13806            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13807            // group head, then wrap the group as a derived table
13808            // (SELECT * FROM (group)) so the outer chain / outer
13809            // tail can't clobber the group's own ordering or limit.
13810            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13811                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13812                    if s.eq_ignore_ascii_case("fetch"));
13813            if has_tail {
13814                self.parse_select_tail_into(&mut head)?;
13815                head = SelectStatement {
13816                    locking: None,
13817                    ctes: Vec::new(),
13818                    distinct: false,
13819                    distinct_on: Vec::new(),
13820                    items: alloc::vec![SelectItem::Wildcard],
13821                    from: Some(FromClause {
13822                        primary: TableRef {
13823                            name: "subquery".to_string(),
13824                            alias: None,
13825                            only: false,
13826                            as_of_segment: None,
13827                            unnest_expr: None,
13828                            unnest_column_aliases: Vec::new(),
13829                            with_ordinality: false,
13830                            generate_series_args: None,
13831                            lateral_subquery: Some(Box::new(head)),
13832                            jsonb_each_text_arg: None,
13833                            table_fn_call: None,
13834                            rows_from: None,
13835                            json_table: None,
13836                            scalar_fn_item: false,
13837                        },
13838                        joins: Vec::new(),
13839                    }),
13840                    where_: None,
13841                    group_by: None,
13842                    group_by_all: false,
13843                    having: None,
13844                    unions: Vec::new(),
13845                    order_by: Vec::new(),
13846                    limit: None,
13847                    offset: None,
13848                    limit_with_ties: false,
13849                    window_check_exprs: Vec::new(),
13850                };
13851            }
13852            if !matches!(self.peek(), Token::RParen) {
13853                return Err(self.err(format!(
13854                    "expected ')' after parenthesized query group, got {:?}",
13855                    self.peek()
13856                )));
13857            }
13858            self.advance();
13859            return Ok(head);
13860        }
13861        // `TABLE name` shorthand as a query block — valid anywhere
13862        // a SELECT head is (set-op peers included).
13863        if matches!(self.peek(), Token::Table)
13864            && matches!(
13865                self.tokens.get(self.pos + 1),
13866                Some(Token::Ident(_) | Token::QuotedIdent(_))
13867            )
13868        {
13869            return self.parse_table_shorthand();
13870        }
13871        if !matches!(self.peek(), Token::Select) {
13872            return Err(self.err(format!(
13873                "expected SELECT to start a query block, got {:?}",
13874                self.peek()
13875            )));
13876        }
13877        self.advance();
13878        // v7.39.9 — MySQL's `SELECT STRAIGHT_JOIN …` join-order hint.
13879        //
13880        // It sits where `DISTINCT` sits and tells the optimiser to join
13881        // in the written order. SPG plans its own joins, so the hint is
13882        // accepted and not acted on — but it has to PARSE, because as a
13883        // bare identifier it became a column: measured on the published
13884        // image, `SELECT STRAIGHT_JOIN a FROM t` answered `Unknown
13885        // column 'straight_join' in 'field list'` where MySQL 9.7.2
13886        // returns the rows. Only in this position, which is the only one
13887        // MySQL accepts either — a trailing `STRAIGHT_JOIN` is its 1064.
13888        if self.mysql_dialect
13889            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("straight_join"))
13890        {
13891            self.advance();
13892        }
13893        let distinct = if matches!(self.peek(), Token::Distinct) {
13894            self.advance();
13895            true
13896        } else {
13897            false
13898        };
13899        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13900        // keep the first row (per ORDER BY) of each group the
13901        // expressions define. Django's .distinct('field') shape.
13902        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13903            self.advance(); // ON
13904            if !matches!(self.peek(), Token::LParen) {
13905                return Err(self.err(format!(
13906                    "expected '(' after DISTINCT ON, got {:?}",
13907                    self.peek()
13908                )));
13909            }
13910            self.advance();
13911            let mut exprs = Vec::new();
13912            loop {
13913                exprs.push(self.parse_expr(0)?);
13914                match self.peek() {
13915                    Token::Comma => {
13916                        self.advance();
13917                    }
13918                    Token::RParen => break,
13919                    other => {
13920                        return Err(self.err(format!(
13921                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13922                        )));
13923                    }
13924                }
13925            }
13926            self.advance(); // )
13927            exprs
13928        } else {
13929            Vec::new()
13930        };
13931        let mut items = self.parse_select_list()?;
13932        // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13933        // of CTAS. It sits exactly here in PG's grammar, right after the
13934        // target list.
13935        //
13936        // A comment in `ast.rs` has said since v7.38 that CTAS and
13937        // `SELECT INTO` lower to the same node. Only CTAS ever did:
13938        // `SELECT i INTO t FROM src` answered `syntax error at or near
13939        // "INTO"`, which the differential found while measuring what
13940        // PostgreSQL tags each of the five materialising forms with. A
13941        // comment describing a capability the code does not have is the
13942        // defect this version has been finding all day, and this is the
13943        // one it found in the parser.
13944        //
13945        // `INTO` is captured rather than consumed here: the name has to
13946        // travel out of a function that returns a `SelectStatement`, and
13947        // the caller lowers the whole thing to the CTAS node.
13948        if matches!(self.peek(), Token::Into) {
13949            self.advance();
13950            // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13951            // the target, not part of its name. SPG has one storage
13952            // class, so `UNLOGGED` is accepted and means nothing, which
13953            // is what it already means on `CREATE TABLE`.
13954            let mut temporary = false;
13955            loop {
13956                match self.peek().clone() {
13957                    Token::Ident(w) | Token::QuotedIdent(w)
13958                        if w.eq_ignore_ascii_case("temp")
13959                            || w.eq_ignore_ascii_case("temporary") =>
13960                    {
13961                        temporary = true;
13962                        self.advance();
13963                    }
13964                    Token::Ident(w) | Token::QuotedIdent(w)
13965                        if w.eq_ignore_ascii_case("unlogged") =>
13966                    {
13967                        self.advance();
13968                    }
13969                    Token::Table => {
13970                        self.advance();
13971                    }
13972                    _ => break,
13973                }
13974            }
13975            let name = match self.peek().clone() {
13976                Token::Ident(w) | Token::QuotedIdent(w) => {
13977                    self.advance();
13978                    w
13979                }
13980                other => {
13981                    return Err(self.err(alloc::format!(
13982                        "expected a table name after SELECT … INTO, got {other:?}"
13983                    )));
13984                }
13985            };
13986            self.pending_select_into = Some((name, temporary));
13987        }
13988        // Scope the TABLESAMPLE lowering channel to this SELECT:
13989        // stash whatever an enclosing select accumulated, collect
13990        // our own FROM's predicates, restore after the combine.
13991        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13992        let mut from = if matches!(self.peek(), Token::From) {
13993            self.advance();
13994            Some(self.parse_from_clause()?)
13995        } else {
13996            None
13997        };
13998        // v7.37 D.22 — a set-returning function in the projection with no FROM
13999        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
14000        // rows. Move the first SRF projection item to a FROM-position derived
14001        // table and replace it in the projection with a reference to its output
14002        // column; sibling scalar columns repeat per SRF row. PG names the output
14003        // column after the function (or its AS alias). Reuses the FROM-SRF
14004        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
14005        // works via the targetlist-SRF path.
14006        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
14007        // `SELECT * FROM f(args)` — the record's fields become the columns, which
14008        // is exactly what the function's own row shape already is. Anywhere else
14009        // (per outer row, or beside other items) it would need a real record-typed
14010        // projection, so it says so rather than answering something else.
14011        if let [
14012            SelectItem::Expr {
14013                expr: Expr::FunctionCall { name, args },
14014                ..
14015            },
14016        ] = items.as_slice()
14017            && name == "__record_expand"
14018        {
14019            let Some(Expr::FunctionCall {
14020                name: inner_name,
14021                args: inner_args,
14022            }) = args.first()
14023            else {
14024                return Err(self.err(
14025                    "(<expr>).* expands a function's record — it needs a function call".into(),
14026                ));
14027            };
14028            if from.is_some() {
14029                return Err(self.err(
14030                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
14031                        .into(),
14032                ));
14033            }
14034            let fn_ref = TableRef {
14035                name: inner_name.clone(),
14036                alias: None,
14037                only: false,
14038                as_of_segment: None,
14039                unnest_expr: None,
14040                unnest_column_aliases: Vec::new(),
14041                with_ordinality: false,
14042                generate_series_args: None,
14043                lateral_subquery: None,
14044                jsonb_each_text_arg: None,
14045                table_fn_call: Some(Box::new((
14046                    inner_name.to_ascii_lowercase(),
14047                    inner_args.clone(),
14048                ))),
14049                rows_from: None,
14050                json_table: None,
14051                scalar_fn_item: false,
14052            };
14053            items = alloc::vec![SelectItem::Wildcard];
14054            from = Some(FromClause {
14055                primary: fn_ref,
14056                joins: Vec::new(),
14057            });
14058        }
14059        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
14060        // FROM, keeps its marker: the ENGINE lowers it, because naming the
14061        // record's fields takes the catalog. It becomes a LATERAL of the same
14062        // function plus one item per declared column — the machinery rounds 65
14063        // and 69 already built.
14064        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
14065        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
14066        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
14067        // express, since the lifted one becomes a scan and the other would
14068        // expand per its rows (a cross product, not a zip). So when the
14069        // projection holds more than one top-level function call, the lift steps
14070        // aside and the engine's target-list expansion takes the whole list.
14071        let fn_call_items = items
14072            .iter()
14073            .filter(|it| {
14074                matches!(
14075                    it,
14076                    SelectItem::Expr {
14077                        expr: Expr::FunctionCall { .. },
14078                        ..
14079                    }
14080                )
14081            })
14082            .count();
14083        if from.is_none() && fn_call_items <= 1 {
14084            let mut found: Option<(usize, TableRef, String)> = None;
14085            for (i, item) in items.iter().enumerate() {
14086                if let SelectItem::Expr {
14087                    expr: Expr::FunctionCall { name, args },
14088                    alias,
14089                } = item
14090                {
14091                    let lname = name.to_ascii_lowercase();
14092                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
14093                    let (unnest, gs) = match lname.as_str() {
14094                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
14095                        "generate_series" if (2..=3).contains(&args.len()) => {
14096                            (None, Some(args.clone()))
14097                        }
14098                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
14099                        // no-FROM projection yields the 1-based subscripts, i.e.
14100                        // generate_series(1, array_length(arr, dim)); an invalid
14101                        // dimension makes array_length NULL → 0 rows, as in PG.
14102                        "generate_subscripts" if args.len() == 2 => (
14103                            None,
14104                            Some(alloc::vec![
14105                                Expr::Literal(Literal::Integer(1)),
14106                                Expr::FunctionCall {
14107                                    name: "array_length".to_string(),
14108                                    args: args.clone(),
14109                                },
14110                            ]),
14111                        ),
14112                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
14113                        // in a no-FROM projection unnest their *_to_array form.
14114                        "string_to_table" | "regexp_split_to_table" => {
14115                            let array_fn = if lname == "string_to_table" {
14116                                "string_to_array"
14117                            } else {
14118                                "regexp_split_to_array"
14119                            };
14120                            (
14121                                Some(Box::new(Expr::FunctionCall {
14122                                    name: array_fn.to_string(),
14123                                    args: args.clone(),
14124                                })),
14125                                None,
14126                            )
14127                        }
14128                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
14129                        // a no-FROM projection expand per element. The scalar form
14130                        // returns the elements as a TEXT array, so unnest over the
14131                        // same call materialises one row each (same rewrite the
14132                        // FROM-clause form uses).
14133                        "jsonb_array_elements"
14134                        | "json_array_elements"
14135                        | "jsonb_array_elements_text"
14136                        | "json_array_elements_text"
14137                            if args.len() == 1 =>
14138                        {
14139                            (
14140                                Some(Box::new(Expr::FunctionCall {
14141                                    name: lname.clone(),
14142                                    args: args.clone(),
14143                                })),
14144                                None,
14145                            )
14146                        }
14147                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
14148                        // in a no-FROM projection expands per match (scalar form
14149                        // returns the matches as a TEXT array → unnest).
14150                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
14151                            Some(Box::new(Expr::FunctionCall {
14152                                name: lname.clone(),
14153                                args: args.clone(),
14154                            })),
14155                            None,
14156                        ),
14157                        _ => continue,
14158                    };
14159                    found = Some((
14160                        i,
14161                        TableRef {
14162                            name: colname.clone(),
14163                            alias: Some(colname.clone()),
14164                            only: false,
14165                            as_of_segment: None,
14166                            unnest_expr: unnest,
14167                            unnest_column_aliases: alloc::vec![colname.clone()],
14168                            with_ordinality: false,
14169                            generate_series_args: gs,
14170                            lateral_subquery: None,
14171                            jsonb_each_text_arg: None,
14172                            table_fn_call: None,
14173                            rows_from: None,
14174                            json_table: None,
14175                            scalar_fn_item: false,
14176                        },
14177                        colname,
14178                    ));
14179                    break;
14180                }
14181            }
14182            if let Some((idx, tref, colname)) = found {
14183                from = Some(FromClause {
14184                    primary: tref,
14185                    joins: Vec::new(),
14186                });
14187                items[idx] = SelectItem::Expr {
14188                    expr: Expr::Column(ColumnName {
14189                        qualifier: None,
14190                        name: colname.clone(),
14191                    }),
14192                    alias: Some(colname),
14193                };
14194            }
14195        }
14196        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
14197        let where_ = if matches!(self.peek(), Token::Where) {
14198            self.advance();
14199            Some(self.parse_expr(0)?)
14200        } else {
14201            None
14202        };
14203        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
14204            Some(match acc {
14205                Some(w) => Expr::Binary {
14206                    lhs: Box::new(pred),
14207                    op: crate::ast::BinOp::And,
14208                    rhs: Box::new(w),
14209                },
14210                None => pred,
14211            })
14212        });
14213        self.pending_sample_preds = enclosing_sample_preds;
14214        let mut group_by_all = false;
14215        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
14216        // share one expansion: `grouping_sets` lists the key subsets
14217        // (first = primary, assigned to stmt.group_by; the rest
14218        // become UNION ALL peers), `grouping_universe` is the full
14219        // key list used to compute each peer's dropped keys.
14220        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
14221        let mut grouping_universe: Vec<Expr> = Vec::new();
14222        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
14223        // A BOOL, not the key list: this frame is the statement parser's, and
14224        // round 430 measured that a `Vec` local here is enough on its own to
14225        // tip the 512 KiB nesting guard. The keys are recoverable from
14226        // `grouping_universe`, which a rollup fills with exactly them.
14227        let mut mysql_rollup = false;
14228        let group_by = if matches!(self.peek(), Token::Group) {
14229            self.advance();
14230            if !self.peek_is_by() {
14231                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
14232            }
14233            self.advance();
14234            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
14235            // every non-aggregate SELECT-list item later.
14236            if matches!(self.peek(), Token::All) {
14237                self.advance();
14238                group_by_all = true;
14239                None
14240            } else {
14241                // v7.39 (round 242) — PG's general grouping-element grammar:
14242                // GROUP BY [DISTINCT] element [, element]*, where an element
14243                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
14244                // SETS (…) — mixed freely. Each element yields a list of
14245                // key sets; the query's grouping sets are the CARTESIAN
14246                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
14247                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
14248                // content. ROLLUP/CUBE members may be composite
14249                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
14250                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
14251                // parser handled only a lone ROLLUP/CUBE/GS as the whole
14252                // clause.
14253                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
14254                    self.advance();
14255                    true
14256                } else {
14257                    false
14258                };
14259                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
14260                loop {
14261                    element_sets.push(self.parse_grouping_element()?);
14262                    if matches!(self.peek(), Token::Comma) {
14263                        self.advance();
14264                    } else {
14265                        break;
14266                    }
14267                }
14268                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
14269                for el in &element_sets {
14270                    let mut next: Vec<Vec<Expr>> = Vec::new();
14271                    for base in &total {
14272                        for set in el {
14273                            let mut merged = base.clone();
14274                            for k in set {
14275                                if !merged.iter().any(|m| m == k) {
14276                                    merged.push(k.clone());
14277                                }
14278                            }
14279                            next.push(merged);
14280                        }
14281                    }
14282                    total = next;
14283                }
14284                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
14285                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
14286                // The keys and the aggregates come out identical; the ROW
14287                // ORDER does not, and that is the part a report depends on.
14288                // MySQL interleaves each group's subtotal right after its
14289                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
14290                // where the union-of-grouping-sets expansion emits every
14291                // leaf first and then every subtotal. MariaDB REFUSES an
14292                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
14293                // order itself — measured on MariaDB 11 and MySQL 9.7, which
14294                // agree on the order and disagree only on whether ORDER BY
14295                // is allowed (MySQL allows it; SPG allows it too, since
14296                // refusing would break the clients that can write it).
14297                if self.mysql_dialect
14298                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
14299                    && matches!(
14300                        self.tokens.get(self.pos + 1),
14301                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
14302                    )
14303                {
14304                    self.advance(); // WITH
14305                    self.advance(); // ROLLUP
14306                    let keys = total.into_iter().next().unwrap_or_default();
14307                    mysql_rollup = true;
14308                    // n+1 prefixes, largest first — the same expansion
14309                    // `ROLLUP (…)` produces.
14310                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
14311                }
14312                if distinct_sets {
14313                    let mut seen: Vec<Vec<String>> = Vec::new();
14314                    total.retain(|set| {
14315                        let mut key: Vec<String> =
14316                            set.iter().map(|e| alloc::format!("{e}")).collect();
14317                        key.sort();
14318                        if seen.contains(&key) {
14319                            false
14320                        } else {
14321                            seen.push(key);
14322                            true
14323                        }
14324                    });
14325                }
14326                if total.len() > 1 {
14327                    let mut universe: Vec<Expr> = Vec::new();
14328                    for set in &total {
14329                        for k in set {
14330                            if !universe.iter().any(|u| u == k) {
14331                                universe.push(k.clone());
14332                            }
14333                        }
14334                    }
14335                    grouping_universe = universe;
14336                    let primary = total[0].clone();
14337                    grouping_sets = total;
14338                    Some(primary)
14339                } else {
14340                    // One set (a plain GROUP BY list, or a single-set
14341                    // spelling like GROUPING SETS ((a, b))). An EMPTY
14342                    // single set — GROUPING SETS (()) — stays
14343                    // `Some(vec![])`: the grand-total group, which must
14344                    // run the aggregate path.
14345                    Some(total.into_iter().next().unwrap_or_default())
14346                }
14347            }
14348        } else {
14349            None
14350        };
14351        let having = if matches!(self.peek(), Token::Having) {
14352            self.advance();
14353            Some(self.parse_expr(0)?)
14354        } else {
14355            None
14356        };
14357        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
14358        // OVER w parsed to a marker above; inline each definition
14359        // into the referencing WindowFunction nodes.
14360        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
14361        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
14362            self.advance();
14363            loop {
14364                let wname = self.expect_ident_like()?;
14365                if !matches!(self.peek(), Token::As) {
14366                    return Err(self.err(format!(
14367                        "expected AS after WINDOW {wname}, got {:?}",
14368                        self.peek()
14369                    )));
14370                }
14371                self.advance();
14372                // v7.39 (round 229) — PG rejects a redefinition outright.
14373                if window_defs
14374                    .iter()
14375                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
14376                {
14377                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
14378                }
14379                let def = self.parse_over_clause()?;
14380                // A definition may itself copy an earlier one
14381                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
14382                // so resolve it against the defs already in scope. Same
14383                // copy rules as an `OVER (w1 …)` in the select list.
14384                let mut probe = Expr::WindowFunction {
14385                    name: String::new(),
14386                    args: Vec::new(),
14387                    partition_by: def.0,
14388                    order_by: def.1,
14389                    frame: def.2,
14390                    null_treatment: crate::ast::NullTreatment::Respect,
14391                    filter: None,
14392                };
14393                Self::substitute_named_windows(&mut probe, &window_defs)
14394                    .map_err(|m| self.err(m))?;
14395                let Expr::WindowFunction {
14396                    partition_by,
14397                    order_by,
14398                    frame,
14399                    ..
14400                } = probe
14401                else {
14402                    unreachable!("probe is a WindowFunction")
14403                };
14404                window_defs.push((wname, (partition_by, order_by, frame)));
14405                if matches!(self.peek(), Token::Comma) {
14406                    self.advance();
14407                    continue;
14408                }
14409                break;
14410            }
14411        }
14412        // v7.39 (round 705) — which definitions did anything reference?
14413        // The ones nothing did used to be dropped here, unexamined, so
14414        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
14415        // definition whether referenced or not. Their key expressions ride
14416        // out on the statement for the engine to resolve.
14417        let mut window_refs: Vec<String> = Vec::new();
14418        if !window_defs.is_empty() {
14419            for it in &items {
14420                if let SelectItem::Expr { expr, .. } = it {
14421                    Self::collect_named_window_refs(expr, &mut window_refs);
14422                }
14423            }
14424        }
14425        let window_check_exprs: Vec<Expr> = window_defs
14426            .iter()
14427            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
14428            .flat_map(|(_, (partition, order, _))| {
14429                partition
14430                    .iter()
14431                    .cloned()
14432                    .chain(order.iter().map(|(e, _, _)| e.clone()))
14433            })
14434            .collect();
14435        if !window_defs.is_empty()
14436            || items
14437                .iter()
14438                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
14439        {
14440            for it in &mut items {
14441                if let SelectItem::Expr { expr, .. } = it {
14442                    Self::substitute_named_windows(expr, &window_defs)
14443                        .map_err(|m| self.err(m))?;
14444                }
14445            }
14446        }
14447        // `GROUP BY 1` — positional keys substitute with the Nth
14448        // select item's expression (same contract ORDER BY has had
14449        // since v6.x). Out-of-range positions error.
14450        let group_by = match group_by {
14451            Some(mut keys) => {
14452                for k in &mut keys {
14453                    if let Expr::Literal(Literal::Integer(n)) = k {
14454                        let idx = *n;
14455                        if idx < 1 || idx as usize > items.len() {
14456                            return Err(self.err(alloc::format!(
14457                                "GROUP BY position {idx} is not in select list"
14458                            )));
14459                        }
14460                        match &items[(idx - 1) as usize] {
14461                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
14462                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
14463                                return Err(self.err(alloc::format!(
14464                                    "GROUP BY position {idx} references a wildcard item"
14465                                )));
14466                            }
14467                        }
14468                    }
14469                }
14470                Some(keys)
14471            }
14472            None => None,
14473        };
14474        let mut stmt = SelectStatement {
14475            locking: None,
14476            ctes: Vec::new(),
14477            distinct,
14478            distinct_on,
14479            items,
14480            from,
14481            where_,
14482            group_by,
14483            group_by_all,
14484            having,
14485            unions: Vec::new(),
14486            order_by: Vec::new(),
14487            limit: None,
14488            offset: None,
14489            limit_with_ties: false,
14490            window_check_exprs,
14491        };
14492        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
14493        // first set is the primary (already on stmt.group_by); each
14494        // further set becomes a UNION ALL peer with its dropped
14495        // keys (universe minus the set) replaced by NULL literals
14496        // in the peer's items and group_by. PG-legal: non-grouped
14497        // select items must be group keys or aggregates, so a
14498        // dropped key's occurrences in the projection are exactly
14499        // the ones to nullify.
14500        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14501        // over a plain GROUP BY (every argument must be a group key; the
14502        // mask is then 0) and rejects anything else with 42803. SPG's
14503        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14504        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14505        // function `grouping`".
14506        if grouping_sets.len() <= 1 {
14507            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14508            let mut calls: Vec<Expr> = Vec::new();
14509            for item in &stmt.items {
14510                if let SelectItem::Expr { expr, .. } = item {
14511                    Self::collect_grouping_calls(expr, &mut calls);
14512                }
14513            }
14514            if let Some(h) = &stmt.having {
14515                Self::collect_grouping_calls(h, &mut calls);
14516            }
14517            for call in &calls {
14518                let Expr::FunctionCall { args, .. } = call else {
14519                    continue;
14520                };
14521                for a in args {
14522                    if !keys.iter().any(|k| k == a) {
14523                        return Err(self.err(
14524                            "arguments to GROUPING must be grouping expressions of the associated query level"
14525                                .to_string(),
14526                        ));
14527                    }
14528                }
14529            }
14530            if !calls.is_empty() {
14531                for item in &mut stmt.items {
14532                    if let SelectItem::Expr { expr, .. } = item {
14533                        Self::substitute_grouping_calls(expr, &[]);
14534                    }
14535                }
14536                if let Some(h) = &mut stmt.having {
14537                    Self::substitute_grouping_calls(h, &[]);
14538                }
14539            }
14540        }
14541        if grouping_sets.len() > 1 {
14542            // The primary set's own dropped keys nullify in the
14543            // HEAD's projection too (GROUPING SETS's first set may
14544            // omit keys other sets use).
14545            let primary = grouping_sets[0].clone();
14546            let head_dropped: Vec<Expr> = grouping_universe
14547                .iter()
14548                .filter(|u| !primary.iter().any(|k| k == *u))
14549                .cloned()
14550                .collect();
14551            for set in grouping_sets.iter().skip(1) {
14552                let mut peer = stmt.clone();
14553                peer.unions = Vec::new();
14554                let dropped: Vec<&Expr> = grouping_universe
14555                    .iter()
14556                    .filter(|u| !set.iter().any(|k| k == *u))
14557                    .collect();
14558                // Empty set = grand-total group: `Some(vec![])` forces
14559                // the aggregate path (one collapsed row) instead of a
14560                // per-row passthrough. See the primary-set note above.
14561                peer.group_by = Some(set.clone());
14562                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14563                for item in &mut peer.items {
14564                    if let SelectItem::Expr { expr, alias } = item {
14565                        if dropped.iter().any(|d| *d == expr) {
14566                            // v7.39 — keep the dropped key's name on the
14567                            // NULL literal so the UNION output column
14568                            // (and any top-level ORDER BY on it) still
14569                            // resolves.
14570                            if alias.is_none()
14571                                && let Expr::Column(c) = &expr
14572                            {
14573                                *alias = Some(c.name.clone());
14574                            }
14575                            *expr = Expr::Literal(Literal::Null);
14576                        } else {
14577                            Self::substitute_grouping_calls(expr, &dropped_owned);
14578                        }
14579                    }
14580                }
14581                if let Some(h) = &mut peer.having {
14582                    Self::substitute_grouping_calls(h, &dropped_owned);
14583                }
14584                stmt.unions.push((UnionKind::All, peer));
14585            }
14586            for item in &mut stmt.items {
14587                if let SelectItem::Expr { expr, alias } = item {
14588                    if head_dropped.iter().any(|d| d == expr) {
14589                        if alias.is_none()
14590                            && let Expr::Column(c) = &expr
14591                        {
14592                            *alias = Some(c.name.clone());
14593                        }
14594                        *expr = Expr::Literal(Literal::Null);
14595                    } else {
14596                        Self::substitute_grouping_calls(expr, &head_dropped);
14597                    }
14598                }
14599            }
14600            if let Some(h) = &mut stmt.having {
14601                Self::substitute_grouping_calls(h, &head_dropped);
14602            }
14603            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14604            // (while `grouping_universe` / the per-branch sets are in scope). For
14605            // each grouping() call in it, inject a per-branch hidden column
14606            // `__grp_ord_K` carrying that branch's mask into the head + every
14607            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14608            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14609            // from the final output. A standalone grouping-set query has ORDER BY
14610            // (not an explicit set-op) next, so consuming it here is safe.
14611            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14612            // rollup carries the hierarchical order: sort by the grouping
14613            // keys with the rolled-up NULLs last, which is exactly the
14614            // interleaving both oracles emit. A client's own ORDER BY wins,
14615            // which is what MySQL does (MariaDB refuses to let one be
14616            // written at all).
14617            // The synthesised keys have to travel the SAME path a written
14618            // ORDER BY does: the block below is what turns a `grouping()`
14619            // call into the per-branch `__grp_ord_K` column the engine can
14620            // actually sort on. Bypassing it left a bare `grouping(text)`
14621            // for the evaluator to reject.
14622            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14623                self.parse_order_by_keys()?
14624            } else if mysql_rollup {
14625                Self::mysql_rollup_order(&grouping_universe)
14626            } else {
14627                Vec::new()
14628            };
14629            if !synthesised_or_parsed.is_empty() {
14630                let mut order_keys = synthesised_or_parsed;
14631                let mut grp_exprs: Vec<Expr> = Vec::new();
14632                for ob in &order_keys {
14633                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14634                }
14635                for (k, gexpr) in grp_exprs.iter().enumerate() {
14636                    let colname = alloc::format!("__grp_ord_{k}");
14637                    // Head branch (primary set) uses `head_dropped`.
14638                    let mut he = gexpr.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                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14645                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14646                        let set = &grouping_sets[i + 1];
14647                        let dropped: Vec<Expr> = grouping_universe
14648                            .iter()
14649                            .filter(|u| !set.iter().any(|k| k == *u))
14650                            .cloned()
14651                            .collect();
14652                        let mut pe = gexpr.clone();
14653                        Self::substitute_grouping_calls(&mut pe, &dropped);
14654                        peer.items.push(SelectItem::Expr {
14655                            expr: pe,
14656                            alias: Some(colname.clone()),
14657                        });
14658                    }
14659                }
14660                for ob in &mut order_keys {
14661                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14662                }
14663                // v7.40.0 — and a KEY the order sorts on that the query
14664                // did not project.
14665                //
14666                // A UNION's ORDER BY can only name output columns, so
14667                // the rollup order synthesised over `grouping_universe`
14668                // named `qty` for `SELECT SUM(qty) … GROUP BY qty WITH
14669                // ROLLUP` and the query answered `column "qty" does not
14670                // exist`. MySQL 9.7.2 answers 0, 1, 2, 3, 6 — it orders
14671                // by the key whether or not it is selected. The key
14672                // travels as a hidden column, exactly as the grouping
14673                // mask above does, and is stripped from the output by
14674                // the same rule.
14675                let mut key_exprs: Vec<Expr> = Vec::new();
14676                for ob in &order_keys {
14677                    let is_key = grouping_universe.iter().any(|u| u == &ob.expr);
14678                    let projected = stmt
14679                        .items
14680                        .iter()
14681                        .any(|it| matches!(it, SelectItem::Expr { expr, .. } if expr == &ob.expr));
14682                    if is_key && !projected && !key_exprs.iter().any(|k| k == &ob.expr) {
14683                        key_exprs.push(ob.expr.clone());
14684                    }
14685                }
14686                for (k, kexpr) in key_exprs.iter().enumerate() {
14687                    let colname = alloc::format!("__grp_key_{k}");
14688                    let mut he = kexpr.clone();
14689                    Self::substitute_grouping_calls(&mut he, &head_dropped);
14690                    stmt.items.push(SelectItem::Expr {
14691                        expr: he,
14692                        alias: Some(colname.clone()),
14693                    });
14694                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14695                        let set = &grouping_sets[i + 1];
14696                        let dropped: Vec<Expr> = grouping_universe
14697                            .iter()
14698                            .filter(|u| !set.iter().any(|kk| kk == *u))
14699                            .cloned()
14700                            .collect();
14701                        let mut pe = kexpr.clone();
14702                        Self::substitute_grouping_calls(&mut pe, &dropped);
14703                        peer.items.push(SelectItem::Expr {
14704                            expr: pe,
14705                            alias: Some(colname.clone()),
14706                        });
14707                    }
14708                    for ob in &mut order_keys {
14709                        if &ob.expr == kexpr {
14710                            ob.expr = Expr::Column(crate::ast::ColumnName {
14711                                name: colname.clone(),
14712                                qualifier: None,
14713                            });
14714                        }
14715                    }
14716                }
14717                stmt.order_by = order_keys;
14718            }
14719        }
14720        Ok(stmt)
14721    }
14722
14723    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14724    /// as ORDER BY keys.
14725    ///
14726    /// Per key: the rollup marker, then the key. Sorting on the key alone
14727    /// is not enough, and a table with a NULL in it says why — MariaDB puts
14728    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14729    /// the ROLLUP-introduced NULL last, and both print as NULL.
14730    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14731    /// real group including the data-NULL one, 1 only for the row the
14732    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14733    /// rolls up to NULL|2, a|1, b|3, NULL|6.
14734    ///
14735    /// `#[inline(never)]`: its locals must not join the statement parser's
14736    /// frame, which round 430 measured sitting against the nesting guard.
14737    #[inline(never)]
14738    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14739        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14740        for e in keys {
14741            out.push(OrderBy {
14742                expr: Expr::FunctionCall {
14743                    name: "grouping".into(),
14744                    args: alloc::vec![e.clone()],
14745                },
14746                desc: false,
14747                nulls_first: None,
14748                collation: None,
14749            });
14750            out.push(OrderBy {
14751                expr: e.clone(),
14752                desc: false,
14753                // MySQL orders NULL first on an ascending key.
14754                nulls_first: Some(true),
14755                collation: None,
14756            });
14757        }
14758        out
14759    }
14760
14761    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14762    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14763    #[inline(never)]
14764    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14765        use crate::ast::MaintainKind;
14766        self.skip_paren_option_list();
14767        let kind = match self.peek() {
14768            // `TABLE` and `INDEX` lex as keywords, not identifiers.
14769            Token::Table | Token::Index => {
14770                self.advance();
14771                MaintainKind::ReindexRelation
14772            }
14773            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14774                "index" | "table" => {
14775                    self.advance();
14776                    MaintainKind::ReindexRelation
14777                }
14778                "schema" => {
14779                    self.advance();
14780                    MaintainKind::ReindexSchema
14781                }
14782                "system" | "database" => {
14783                    self.advance();
14784                    MaintainKind::Whole
14785                }
14786                // PG requires the object type; anything else is the
14787                // caller's problem, not something to swallow.
14788                _ => MaintainKind::ReindexRelation,
14789            },
14790            _ => MaintainKind::Whole,
14791        };
14792        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14793        // allows the plain form, so the modifier is recorded rather than
14794        // skipped. It still has no effect on how the reindex runs.
14795        let mut concurrently = false;
14796        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14797            self.advance();
14798            concurrently = true;
14799        }
14800        let target = self.take_optional_maintain_name();
14801        self.consume_until_statement_boundary();
14802        Ok(Statement::Maintain {
14803            kind,
14804            concurrently,
14805            target,
14806        })
14807    }
14808
14809    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14810    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14811    #[inline(never)]
14812    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14813        use crate::ast::MaintainKind;
14814        self.skip_paren_option_list();
14815        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14816            self.advance();
14817        }
14818        let target = self.take_optional_maintain_name();
14819        self.consume_until_statement_boundary();
14820        Ok(Statement::Maintain {
14821            kind: if target.is_some() {
14822                MaintainKind::ClusterRelation
14823            } else {
14824                MaintainKind::Whole
14825            },
14826            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14827            // transaction block quite happily (measured).
14828            concurrently: false,
14829            target,
14830        })
14831    }
14832
14833    /// The next token as a relation / schema name, when there is one.
14834    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14835        match self.peek() {
14836            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14837                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14838                _ => None,
14839            },
14840            _ => None,
14841        }
14842    }
14843
14844    /// A parenthesised option list, absorbed.
14845    fn skip_paren_option_list(&mut self) {
14846        if !matches!(self.peek(), Token::LParen) {
14847            return;
14848        }
14849        let mut depth = 0usize;
14850        loop {
14851            match self.advance() {
14852                Token::LParen => depth += 1,
14853                Token::RParen => {
14854                    depth -= 1;
14855                    if depth == 0 {
14856                        return;
14857                    }
14858                }
14859                Token::Eof => return,
14860                _ => {}
14861            }
14862        }
14863    }
14864
14865    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14866    /// column list.
14867    ///
14868    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14869    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14870    /// / ALL. The three that describe physical storage have no meaning
14871    /// here, so they parse and change nothing rather than making a
14872    /// dump that mentions them fail to load.
14873    ///
14874    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14875    /// parse chain the nesting sentinel is tuned against.
14876    #[inline(never)]
14877    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14878        self.advance(); // LIKE
14879        let source = self.expect_ident_like()?;
14880        let mut options = crate::ast::LikeOptions::default();
14881        loop {
14882            let including = match self.peek() {
14883                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14884                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14885                _ => break,
14886            };
14887            self.advance();
14888            // `ALL` lexes as its own keyword, not an identifier.
14889            let opt = if matches!(self.peek(), Token::All) {
14890                self.advance();
14891                alloc::string::String::from("all")
14892            } else {
14893                self.expect_ident_like()?
14894            };
14895            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14896                o.defaults = on;
14897                o.constraints = on;
14898                o.identity = on;
14899                o.generated = on;
14900                o.indexes = on;
14901                o.comments = on;
14902            };
14903            match opt.to_ascii_lowercase().as_str() {
14904                "all" => set(&mut options, including),
14905                "defaults" => options.defaults = including,
14906                "constraints" => options.constraints = including,
14907                "identity" => options.identity = including,
14908                "generated" => options.generated = including,
14909                "indexes" => options.indexes = including,
14910                "comments" => options.comments = including,
14911                // No storage model to copy into.
14912                "storage" | "statistics" | "compression" => {}
14913                other => {
14914                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14915                }
14916            }
14917        }
14918        Ok(crate::ast::LikeSpec {
14919            source,
14920            at,
14921            options,
14922            keep_index_names: false,
14923        })
14924    }
14925
14926    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14927        // Caller already consumed CREATE; we're sitting on TABLE.
14928        debug_assert!(matches!(self.peek(), Token::Table));
14929        self.advance();
14930        let if_not_exists = self.consume_if_not_exists();
14931        let name = self.expect_ident_like()?;
14932        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14933        // child shape has no column list; the child inherits its
14934        // columns from the parent at engine-DDL time. Detect it
14935        // before the `(` requirement below.
14936        if matches!(self.peek(), Token::Partition)
14937            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14938        {
14939            self.advance(); // PARTITION
14940            self.advance(); // of
14941            let partition_of = self.parse_partition_of_tail()?;
14942            return Ok(Statement::CreateTable(CreateTableStatement {
14943                temporary: false,
14944                name,
14945                engine: None,
14946                auto_increment: None,
14947                columns: Vec::new(),
14948                like_specs: Vec::new(),
14949                inherits: Vec::new(),
14950                if_not_exists,
14951                foreign_keys: Vec::new(),
14952                table_constraints: Vec::new(),
14953                partition_by: None,
14954                partition_of: Some(partition_of),
14955            }));
14956        }
14957        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14958        // the materialized-view materialisation path (run the SELECT, infer the
14959        // column types, create + populate the table) but marks the node so the
14960        // executor creates a plain table without a mat-view registry entry.
14961        if matches!(self.peek(), Token::As) {
14962            self.advance();
14963            let body_stmt = self.parse_select_stmt()?;
14964            let Statement::Select(body) = body_stmt else {
14965                return Err(self.err(format!(
14966                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14967                )));
14968            };
14969            let with_data = self.parse_optional_with_data(true)?;
14970            return Ok(Statement::CreateMaterializedView(
14971                crate::ast::CreateMaterializedViewStatement {
14972                    temporary: false,
14973                    name,
14974                    if_not_exists,
14975                    columns: Vec::new(),
14976                    body,
14977                    with_data,
14978                    as_plain_table: true,
14979                },
14980            ));
14981        }
14982        // v7.40.0 — MySQL's `CREATE TABLE b LIKE a`, which is the same
14983        // copy PostgreSQL spells `CREATE TABLE b (LIKE a INCLUDING ALL)`
14984        // written without the parentheses. It was a syntax error, so a
14985        // schema written against MySQL could not be loaded at all.
14986        //
14987        // Measured on MySQL 9.7.2: the copy takes the columns, their
14988        // defaults and the indexes, and takes neither the rows nor the
14989        // foreign keys — which is exactly `INCLUDING ALL` here, since
14990        // SPG's LIKE has never copied foreign keys.
14991        if matches!(self.peek(), Token::Like) {
14992            let at = self.pos;
14993            let spec = self.parse_create_table_like(at)?;
14994            let spec = crate::ast::LikeSpec {
14995                options: crate::ast::LikeOptions {
14996                    defaults: true,
14997                    constraints: true,
14998                    identity: true,
14999                    generated: true,
15000                    indexes: true,
15001                    comments: true,
15002                },
15003                keep_index_names: true,
15004                ..spec
15005            };
15006            return Ok(Statement::CreateTable(CreateTableStatement {
15007                temporary: false,
15008                name,
15009                engine: None,
15010                auto_increment: None,
15011                columns: Vec::new(),
15012                like_specs: alloc::vec![spec],
15013                inherits: Vec::new(),
15014                if_not_exists,
15015                foreign_keys: Vec::new(),
15016                table_constraints: Vec::new(),
15017                partition_by: None,
15018                partition_of: None,
15019            }));
15020        }
15021        if !matches!(self.peek(), Token::LParen) {
15022            return Err(self.err(format!(
15023                "expected '(' after table name, got {:?}",
15024                self.peek()
15025            )));
15026        }
15027        self.advance();
15028        let mut columns = Vec::new();
15029        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
15030        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
15031        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
15032        loop {
15033            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
15034            // column list. It is how a child that adds nothing of its own is
15035            // written, and this loop demanded at least one entry: `syntax
15036            // error at or near ")"`. The child takes the parent's columns,
15037            // which the INHERITS clause already arranges.
15038            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
15039                self.advance();
15040                break;
15041            }
15042            // v7.6.0 / v7.9.18 — distinguish table-level constraint
15043            // clauses from column definitions. Constraints start
15044            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
15045            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
15046            // a column.
15047            if self.peek_table_level_pk_start() {
15048                table_constraints.push(self.parse_table_level_primary_key()?);
15049            } else if matches!(self.peek(), Token::Like) {
15050                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
15051                // <opt> ]*`. The source table's shape lives in the catalog,
15052                // so this records the clause and the engine expands it.
15053                like_specs.push(self.parse_create_table_like(columns.len())?);
15054            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
15055                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
15056                table_constraints.push(self.parse_table_level_exclude()?);
15057            } else if self.peek_table_level_unique_start() {
15058                table_constraints.push(self.parse_table_level_unique()?);
15059            } else if self.peek_table_level_check_start() {
15060                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
15061                table_constraints.push(self.parse_table_level_check()?);
15062            } else if self.peek_mysql_inline_key_start() {
15063                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
15064                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
15065                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
15066                // inside the column list. Skip name + paren list;
15067                // for UNIQUE KEY, register as a UC.
15068                if let Some(uc) = self.parse_mysql_inline_key()? {
15069                    table_constraints.push(uc);
15070                }
15071            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
15072                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
15073                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
15074                // CHECK is named, and the named-CONSTRAINT arm used
15075                // to accept FOREIGN KEY only. The name is accepted
15076                // and discarded — same handling as every other SPG
15077                // constraint name.
15078                self.advance(); // CONSTRAINT
15079                // v7.39 (read01 round 48) — the name is kept now: the schema
15080                // stores it, so DROP / RENAME CONSTRAINT can find it.
15081                let con_name = self.expect_ident_like()?;
15082                let mut tc = match kind {
15083                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
15084                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
15085                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
15086                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
15087                };
15088                match &mut tc {
15089                    crate::ast::TableConstraint::Check { name, .. }
15090                    | crate::ast::TableConstraint::Unique { name, .. }
15091                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
15092                    | crate::ast::TableConstraint::Exclude { name, .. } => {
15093                        *name = Some(con_name);
15094                    }
15095                    _ => {}
15096                }
15097                table_constraints.push(tc);
15098            } else if self.peek_constraint_or_fk_start() {
15099                foreign_keys.push(self.parse_table_level_fk()?);
15100            } else {
15101                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
15102                // v7.13.0 — fold inline UNIQUE / CHECK column
15103                // constraints into table-level entries so the
15104                // engine path stays uniform.
15105                if col.is_unique {
15106                    table_constraints.push(crate::ast::TableConstraint::Unique {
15107                        name: None,
15108                        columns: alloc::vec![col.name.clone()],
15109                        nulls_not_distinct: col.unique_nulls_not_distinct,
15110                        deferrable: col.constraint_deferrable,
15111                        initially_deferred: col.constraint_initially_deferred,
15112                        prefix_lengths: Vec::new(),
15113                    });
15114                }
15115                if let Some(check_expr) = col.check.clone() {
15116                    table_constraints.push(crate::ast::TableConstraint::Check {
15117                        name: None,
15118                        expr: check_expr,
15119                        not_valid: false,
15120                    });
15121                }
15122                columns.push(col);
15123                if let Some(fk) = col_level_fk {
15124                    foreign_keys.push(fk);
15125                }
15126            }
15127            match self.peek() {
15128                Token::Comma => {
15129                    self.advance();
15130                }
15131                Token::RParen => {
15132                    self.advance();
15133                    break;
15134                }
15135                other => {
15136                    return Err(
15137                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
15138                    );
15139                }
15140            }
15141        }
15142        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
15143        // `CREATE TABLE k (LIKE t)` is a complete definition even though
15144        // nothing is written between the parentheses.
15145        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
15146        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
15147        // empty parentheses were a parse error in their own right — quite apart
15148        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
15149        // SPG does not have (filed separately).
15150        let _ = &like_specs;
15151        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
15152        // It sits between the column list and the MySQL table options,
15153        // and it was a syntax error until this round.
15154        let mut inherits: Vec<String> = Vec::new();
15155        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
15156            if k.eq_ignore_ascii_case("inherits"))
15157        {
15158            self.advance();
15159            if !matches!(self.peek(), Token::LParen) {
15160                return Err(self.err(alloc::format!(
15161                    "expected ( after INHERITS, got {:?}",
15162                    self.peek()
15163                )));
15164            }
15165            self.advance();
15166            loop {
15167                inherits.push(self.expect_ident_like()?);
15168                if matches!(self.peek(), Token::Comma) {
15169                    self.advance();
15170                    continue;
15171                }
15172                break;
15173            }
15174            if !matches!(self.peek(), Token::RParen) {
15175                return Err(self.err(alloc::format!(
15176                    "expected ) closing INHERITS, got {:?}",
15177                    self.peek()
15178                )));
15179            }
15180            self.advance();
15181        }
15182        // v7.14.0 — consume MySQL/MariaDB table options after the
15183        // closing `)`. mysqldump emits things like
15184        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
15185        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
15186        // SPG accepts all forms as no-ops (each option is
15187        // `<ident> [=] <ident-or-string>` separated by whitespace).
15188        let (engine, auto_increment) = self.consume_mysql_table_options();
15189        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
15190        // SPG has no per-table reloptions, so accept and ignore them so a
15191        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
15192        self.consume_with_reloptions();
15193        // v7.37.6-B — declarative-partition-parent suffix
15194        // (`PARTITION BY RANGE (key_col)`) sits after the column
15195        // list + MySQL table-options. v7.37.6-B only accepts RANGE
15196        // and locks the key column at one ident; the engine then
15197        // verifies the column type is TIMESTAMPTZ.
15198        let partition_by = if matches!(self.peek(), Token::Partition) {
15199            self.advance(); // PARTITION
15200            if !self.peek_is_by() {
15201                return Err(self.err(format!(
15202                    "expected BY after PARTITION, got {:?}",
15203                    self.peek()
15204                )));
15205            }
15206            self.advance();
15207            Some(self.parse_partition_by_tail()?)
15208        } else {
15209            None
15210        };
15211        Ok(Statement::CreateTable(CreateTableStatement {
15212            temporary: false,
15213            name,
15214            engine,
15215            auto_increment,
15216            columns,
15217            like_specs,
15218            inherits,
15219            if_not_exists,
15220            foreign_keys,
15221            table_constraints,
15222            partition_by,
15223            partition_of: None,
15224        }))
15225    }
15226
15227    /// v7.37.6-B — case-insensitive ident match helper for the
15228    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
15229    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
15230    /// didn't burn a global keyword slot for each (see the
15231    /// `Token::Partition` doc-comment in `lexer.rs`).
15232    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
15233        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
15234    }
15235
15236    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
15237    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
15238    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
15239        use crate::ast::{PartitionBySpec, PartitionKindAst};
15240        let kind = match self.peek() {
15241            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
15242                self.advance();
15243                PartitionKindAst::Range
15244            }
15245            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
15246                self.advance();
15247                PartitionKindAst::List
15248            }
15249            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
15250                self.advance();
15251                PartitionKindAst::Hash
15252            }
15253            other => {
15254                return Err(self.err(format!(
15255                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
15256                )));
15257            }
15258        };
15259        if !matches!(self.peek(), Token::LParen) {
15260            return Err(self.err(format!(
15261                "expected '(' after PARTITION BY <strategy>, got {:?}",
15262                self.peek()
15263            )));
15264        }
15265        self.advance();
15266        let mut key_columns = Vec::new();
15267        loop {
15268            key_columns.push(self.expect_ident_like()?);
15269            match self.peek() {
15270                Token::Comma => {
15271                    self.advance();
15272                }
15273                Token::RParen => {
15274                    self.advance();
15275                    break;
15276                }
15277                other => {
15278                    return Err(self.err(format!(
15279                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
15280                    )));
15281                }
15282            }
15283        }
15284        if key_columns.is_empty() {
15285            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
15286        }
15287        Ok(PartitionBySpec { kind, key_columns })
15288    }
15289
15290    /// v7.37.6-B — after `PARTITION OF`, expect
15291    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
15292    /// or
15293    ///   <parent> DEFAULT
15294    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
15295        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
15296        let parent_name = self.expect_ident_like()?;
15297        // v7.37.6-B rejects an explicit column list — the child
15298        // inherits from the parent. mailrs round-7 taught us that
15299        // CREATE TABLE-side schema reconciliation hides drift, so
15300        // we surface this as a parse error rather than silently
15301        // ignoring user columns.
15302        if matches!(self.peek(), Token::LParen) {
15303            return Err(self.err(
15304                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
15305                 at v7.37.6-B; the child inherits its columns from the parent"
15306                    .to_string(),
15307            ));
15308        }
15309        let bounds = match self.peek() {
15310            Token::Default => {
15311                self.advance();
15312                PartitionOfBoundsAst::Default
15313            }
15314            Token::For => {
15315                self.advance();
15316                if !matches!(self.peek(), Token::Values) {
15317                    return Err(
15318                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
15319                    );
15320                }
15321                self.advance();
15322                // WITH is not a reserved Token in the lexer — it lexes
15323                // as Token::Ident("with"). Disambiguate manually.
15324                let want_with = matches!(
15325                    self.peek(),
15326                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15327                );
15328                if want_with {
15329                    self.advance();
15330                    if !matches!(self.peek(), Token::LParen) {
15331                        return Err(self.err(format!(
15332                            "expected '(' after FOR VALUES WITH, got {:?}",
15333                            self.peek()
15334                        )));
15335                    }
15336                    self.advance();
15337                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
15338                    loop {
15339                        let key = self.expect_ident_like()?;
15340                        let n = match self.peek().clone() {
15341                            Token::Integer(v) if u32::try_from(v).is_ok() => {
15342                                self.advance();
15343                                v as u32
15344                            }
15345                            other => {
15346                                return Err(self.err(format!(
15347                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
15348                                )));
15349                            }
15350                        };
15351                        match key.to_ascii_uppercase().as_str() {
15352                            "MODULUS" => modulus = Some(n),
15353                            "REMAINDER" => remainder = Some(n),
15354                            other => {
15355                                return Err(self.err(format!(
15356                                    "FOR VALUES WITH: unknown key {other:?}; \
15357                                     expected MODULUS or REMAINDER"
15358                                )));
15359                            }
15360                        }
15361                        match self.peek() {
15362                            Token::Comma => {
15363                                self.advance();
15364                            }
15365                            Token::RParen => {
15366                                self.advance();
15367                                break;
15368                            }
15369                            other => {
15370                                return Err(self.err(format!(
15371                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
15372                                )));
15373                            }
15374                        }
15375                    }
15376                    let modulus = modulus
15377                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
15378                    let remainder = remainder.ok_or_else(|| {
15379                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
15380                    })?;
15381                    if modulus == 0 {
15382                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
15383                    }
15384                    if remainder >= modulus {
15385                        return Err(self.err(format!(
15386                            "FOR VALUES WITH: REMAINDER ({remainder}) \
15387                             must be < MODULUS ({modulus})"
15388                        )));
15389                    }
15390                    PartitionOfBoundsAst::Hash { modulus, remainder }
15391                } else {
15392                    match self.peek() {
15393                        Token::From => {
15394                            self.advance();
15395                            let lower = Box::new(self.parse_partition_bound_expr()?);
15396                            if !matches!(self.peek(), Token::To) {
15397                                return Err(self.err(format!(
15398                                    "expected TO after FROM (...), got {:?}",
15399                                    self.peek()
15400                                )));
15401                            }
15402                            self.advance();
15403                            let upper = Box::new(self.parse_partition_bound_expr()?);
15404                            PartitionOfBoundsAst::Range { lower, upper }
15405                        }
15406                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
15407                        Token::In => {
15408                            self.advance();
15409                            if !matches!(self.peek(), Token::LParen) {
15410                                return Err(self.err(format!(
15411                                    "expected '(' after FOR VALUES IN, got {:?}",
15412                                    self.peek()
15413                                )));
15414                            }
15415                            self.advance();
15416                            let mut values = Vec::new();
15417                            loop {
15418                                values.push(self.parse_expr(0)?);
15419                                match self.peek() {
15420                                    Token::Comma => {
15421                                        self.advance();
15422                                    }
15423                                    Token::RParen => {
15424                                        self.advance();
15425                                        break;
15426                                    }
15427                                    other => {
15428                                        return Err(self.err(format!(
15429                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
15430                                    )));
15431                                    }
15432                                }
15433                            }
15434                            if values.is_empty() {
15435                                return Err(self.err(
15436                                    "FOR VALUES IN requires at least one literal".to_string(),
15437                                ));
15438                            }
15439                            PartitionOfBoundsAst::List { values }
15440                        }
15441                        other => {
15442                            return Err(self.err(format!(
15443                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
15444                            )));
15445                        }
15446                    }
15447                }
15448            }
15449            other => {
15450                return Err(self.err(format!(
15451                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
15452                )));
15453            }
15454        };
15455        Ok(PartitionOfSpec {
15456            parent_name,
15457            bounds,
15458        })
15459    }
15460
15461    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
15462    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
15463    /// markers (no-arg builtins) so the engine resolves them
15464    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
15465    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
15466        if !matches!(self.peek(), Token::LParen) {
15467            return Err(self.err(format!(
15468                "expected '(' before partition bound, got {:?}",
15469                self.peek()
15470            )));
15471        }
15472        self.advance();
15473        let expr = match self.peek() {
15474            Token::Ident(s) | Token::QuotedIdent(s)
15475                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
15476            {
15477                let name = s.to_ascii_uppercase();
15478                self.advance();
15479                crate::ast::Expr::FunctionCall {
15480                    name,
15481                    args: Vec::new(),
15482                }
15483            }
15484            _ => self.parse_expr(0)?,
15485        };
15486        if !matches!(self.peek(), Token::RParen) {
15487            return Err(self.err(format!(
15488                "expected ')' after partition bound, got {:?}",
15489                self.peek()
15490            )));
15491        }
15492        self.advance();
15493        Ok(expr)
15494    }
15495
15496    /// v7.14.0 — true when the next tokens look like an inline
15497    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
15498    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
15499    /// — each followed by an optional name + `(...)`. Critical:
15500    /// a column NAMED `key` / `index` (PG accepts as ident) must
15501    /// NOT be mistaken for the KEY constraint shape. We disambig
15502    /// by requiring the keyword to be followed by either `(` or
15503    /// `<ident> (`.
15504    fn peek_mysql_inline_key_start(&self) -> bool {
15505        let cur = self.peek();
15506        // Shapes:
15507        //   KEY (cols)
15508        //   KEY name (cols)
15509        //   INDEX (cols)
15510        //   INDEX name (cols)
15511        //   UNIQUE KEY [name] (cols)
15512        //   UNIQUE INDEX [name] (cols)
15513        //   FULLTEXT [KEY|INDEX] [name] (cols)
15514        //   SPATIAL [KEY|INDEX] [name] (cols)
15515        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
15516            // tokens at skip = the position AFTER the index-form
15517            // keywords (KEY/INDEX) have been consumed.
15518            match self.tokens.get(skip) {
15519                Some(Token::LParen) => true,
15520                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
15521                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
15522                }
15523                _ => false,
15524            }
15525        };
15526        // `INDEX` lexes as Token::Index (reserved), not as
15527        // Token::Ident("index"). Both shapes count as a KEY/INDEX
15528        // start; the peek helper below handles either.
15529        let is_key_or_index_tok = |t: &Token| -> bool {
15530            matches!(t, Token::Index)
15531                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
15532        };
15533        match cur {
15534            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
15535            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15536                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
15537            }
15538            Token::Ident(s)
15539                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
15540            {
15541                let nxt = self.tokens.get(self.pos + 1);
15542                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
15543                    self.pos + 2
15544                } else {
15545                    self.pos + 1
15546                };
15547                after_keyword_followed_by_paren_or_ident_paren(after_after)
15548            }
15549            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
15550                let nxt = self.tokens.get(self.pos + 1);
15551                if !nxt.is_some_and(is_key_or_index_tok) {
15552                    return false;
15553                }
15554                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
15555            }
15556            _ => false,
15557        }
15558    }
15559
15560    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
15561    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
15562    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
15563    /// returns Some(TableConstraint::Index) so the engine builds
15564    /// a real BTree index on the leading column (mysqldump
15565    /// `KEY idx_posts_author (author_id)` shape).
15566    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
15567    /// (the storage layer has no matching AM).
15568    fn parse_mysql_inline_key(
15569        &mut self,
15570    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
15571        // Detect UNIQUE prefix.
15572        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
15573        {
15574            self.advance();
15575            true
15576        } else {
15577            false
15578        };
15579        // Consume FULLTEXT / SPATIAL prefix and record which one
15580        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
15581        // dedicated TableConstraint variant so the engine can
15582        // build a tsvector-GIN; SPATIAL still has no matching
15583        // AM, so it falls back to accept-as-no-op.
15584        let mut is_fulltext = false;
15585        let mut is_spatial = false;
15586        if let Token::Ident(s) = self.peek().clone() {
15587            if s.eq_ignore_ascii_case("fulltext") {
15588                self.advance();
15589                is_fulltext = true;
15590            } else if s.eq_ignore_ascii_case("spatial") {
15591                self.advance();
15592                is_spatial = true;
15593            }
15594        }
15595        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
15596        // (reserved); accept either token shape.
15597        match self.peek() {
15598            Token::Index => {
15599                self.advance();
15600            }
15601            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15602                self.advance();
15603            }
15604            other => {
15605                return Err(self.err(alloc::format!(
15606                    "expected KEY/INDEX in inline index declaration, got {other:?}"
15607                )));
15608            }
15609        }
15610        // Optional index name (an ident before the `(`).
15611        // v7.15.0 — capture the name when present so the engine
15612        // builds the secondary index under the user's chosen
15613        // name (matches mysqldump's `KEY idx_x (col)` shape).
15614        let mut idx_name: Option<String> = None;
15615        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15616            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15617        {
15618            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15619                idx_name = Some(s);
15620            }
15621        }
15622        // Optional `USING BTREE` / `USING HASH` (MySQL).
15623        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15624            self.advance();
15625            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15626                self.advance();
15627            }
15628        }
15629        // Required column list `(col [, col]*)`.
15630        if !matches!(self.peek(), Token::LParen) {
15631            return Err(self.err(alloc::format!(
15632                "expected '(' in inline KEY/INDEX, got {:?}",
15633                self.peek()
15634            )));
15635        }
15636        self.advance();
15637        let mut cols: Vec<String> = Vec::new();
15638        let mut prefix_lengths: Vec<Option<u32>> = Vec::new();
15639        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15640            self.advance();
15641            cols.push(s);
15642            // v7.40.0 — the per-column `(length)` prefix is KEPT.
15643            //
15644            // It used to be skipped, so `KEY kb (b(4))` was accepted and
15645            // the prefix forgotten: `SHOW INDEX` reported `Sub_part`
15646            // NULL and `SHOW CREATE TABLE` printed `(b)` where MySQL
15647            // 9.7.2 prints `(b(4))`. A declaration that is accepted and
15648            // then unrecorded is the worst of the three answers.
15649            let mut prefix: Option<u32> = None;
15650            if matches!(self.peek(), Token::LParen) {
15651                let mut depth = 1usize;
15652                self.advance();
15653                if let Token::Integer(n) = self.peek()
15654                    && let Ok(v) = u32::try_from(*n)
15655                {
15656                    prefix = Some(v);
15657                }
15658                while depth > 0 {
15659                    match self.peek() {
15660                        Token::LParen => depth += 1,
15661                        Token::RParen => depth -= 1,
15662                        Token::Eof => break,
15663                        _ => {}
15664                    }
15665                    self.advance();
15666                }
15667            }
15668            prefix_lengths.push(prefix);
15669            // Skip optional ASC / DESC.
15670            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15671                || matches!(self.peek(), Token::Asc | Token::Desc)
15672            {
15673                self.advance();
15674            }
15675            if matches!(self.peek(), Token::Comma) {
15676                self.advance();
15677                continue;
15678            }
15679            break;
15680        }
15681        if matches!(self.peek(), Token::RParen) {
15682            self.advance();
15683        }
15684        // Trailing options on the inline index — comment / etc.
15685        // Skip until comma or `)`.
15686        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15687            self.advance();
15688        }
15689        if cols.is_empty() {
15690            return Ok(None);
15691        }
15692        if is_unique {
15693            // Carry the captured idx_name on UNIQUE too so future
15694            // engine work can name the underlying BTree
15695            // accordingly; today the unique-constraint installer
15696            // synthesises the name itself, but Display round-trip
15697            // benefits from preserving it.
15698            Ok(Some(crate::ast::TableConstraint::Unique {
15699                name: idx_name,
15700                columns: cols,
15701                nulls_not_distinct: false,
15702                // MySQL inline UNIQUE KEY has no deferral vocabulary.
15703                deferrable: false,
15704                initially_deferred: false,
15705                prefix_lengths,
15706            }))
15707        } else if is_fulltext {
15708            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15709            // routes through `TableConstraint::FulltextIndex`;
15710            // the engine builds a tsvector-GIN over each named
15711            // column so MATCH AGAINST gets a real inverted
15712            // index instead of a silently-dropped declaration.
15713            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15714                name: idx_name,
15715                columns: cols,
15716            }))
15717        } else if is_spatial {
15718            // SPG has no native SPATIAL AM. Accept-as-no-op
15719            // (declaration is parsed, but no index is built).
15720            Ok(None)
15721        } else {
15722            // v7.15.0 — plain KEY / INDEX builds a real BTree
15723            // secondary index.
15724            Ok(Some(crate::ast::TableConstraint::Index {
15725                name: idx_name,
15726                columns: cols,
15727                prefix_lengths,
15728            }))
15729        }
15730    }
15731
15732    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15733    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15734    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15735    /// (in any order, separated by whitespace).
15736    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15737    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15738    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15739    /// bare ident here, and only the parenthesised form is reloptions (so this
15740    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15741    fn consume_with_reloptions(&mut self) {
15742        let is_with = matches!(
15743            self.peek(),
15744            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15745        );
15746        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15747            return;
15748        }
15749        self.advance(); // WITH
15750        self.advance(); // (
15751        let mut depth = 1u32;
15752        while depth > 0 && !matches!(self.peek(), Token::Eof) {
15753            match self.peek() {
15754                Token::LParen => depth += 1,
15755                Token::RParen => depth -= 1,
15756                _ => {}
15757            }
15758            self.advance();
15759        }
15760    }
15761
15762    /// v7.39 — returns the `ENGINE=` name, which used to be consumed and
15763    /// dropped with everything else here. The rest of the MySQL table
15764    /// options genuinely have no meaning for SPG's storage; the engine
15765    /// name does, because MySQL REFUSES one it does not know and a dump
15766    /// with a typo in it should not quietly become a table.
15767    fn consume_mysql_table_options(&mut self) -> (Option<alloc::string::String>, Option<i64>) {
15768        let mut engine: Option<alloc::string::String> = None;
15769        let mut auto_increment: Option<i64> = None;
15770        loop {
15771            // Heuristic: a table option is an ident (or `DEFAULT`
15772            // reserved keyword) followed by `=` and an
15773            // ident / string / integer.
15774            let name_lc = match self.peek().clone() {
15775                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15776                Token::Default => alloc::string::String::from("default"),
15777                _ => break,
15778            };
15779            let known = matches!(
15780                name_lc.as_str(),
15781                "engine"
15782                    | "default"
15783                    | "charset"
15784                    | "collate"
15785                    | "auto_increment"
15786                    | "row_format"
15787                    | "comment"
15788                    | "pack_keys"
15789                    | "stats_persistent"
15790                    | "stats_auto_recalc"
15791                    | "stats_sample_pages"
15792                    | "key_block_size"
15793                    | "tablespace"
15794                    | "min_rows"
15795                    | "max_rows"
15796                    | "checksum"
15797                    | "delay_key_write"
15798                    | "insert_method"
15799                    | "data"
15800                    | "index"
15801                    | "encryption"
15802                    | "compression"
15803            );
15804            if !known {
15805                break;
15806            }
15807            self.advance(); // option name
15808            // `DEFAULT` optional prefix is followed by `CHARSET` /
15809            // `COLLATE`; consume the next ident too.
15810            if name_lc == "default" {
15811                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15812                    self.advance();
15813                }
15814            }
15815            if matches!(self.peek(), Token::Eq) {
15816                self.advance();
15817            }
15818            match self.peek().clone() {
15819                Token::Ident(v) | Token::QuotedIdent(v) | Token::String(v) => {
15820                    if name_lc == "engine" {
15821                        // v7.39.3 — as WRITTEN. MySQL 9.7.2 refuses an
15822                        // engine it does not know and names it back
15823                        // exactly: `Unknown storage engine 'NoSuchEng'`,
15824                        // measured. The lexer folds a bare identifier, so
15825                        // the message quoted a name the dump did not
15826                        // contain, which is the one thing that message is
15827                        // for. Guarded the same way the column spelling
15828                        // is: the span runs to the next token, so what
15829                        // comes back has to be the same word.
15830                        let written = self
15831                            .source_span(self.pos, self.pos)
15832                            .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
15833                            .filter(|raw| raw.eq_ignore_ascii_case(&v))
15834                            .map(alloc::string::String::from);
15835                        engine = Some(written.unwrap_or(v));
15836                    }
15837                    self.advance();
15838                }
15839                Token::Integer(v) => {
15840                    // v7.40.0 — `AUTO_INCREMENT=100` is the next value
15841                    // the table hands out, and it was consumed and
15842                    // dropped: measured on MySQL 9.7.2, the first row
15843                    // inserted into a table declared that way gets 100,
15844                    // where SPG gave it 1. `SHOW CREATE TABLE` already
15845                    // reproduces the option from the counter, so the
15846                    // dump round-tripped through a different number.
15847                    if name_lc == "auto_increment" {
15848                        auto_increment = Some(v);
15849                    }
15850                    self.advance();
15851                }
15852                _ => {}
15853            }
15854        }
15855        (engine, auto_increment)
15856    }
15857
15858    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15859    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15860    /// sure (otherwise a column literally named `primary` would
15861    /// be mistaken).
15862    fn peek_table_level_pk_start(&self) -> bool {
15863        let cur = self.peek();
15864        let nxt = self.tokens.get(self.pos + 1);
15865        let nxt2 = self.tokens.get(self.pos + 2);
15866        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15867        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15868        let is_lparen = matches!(nxt2, Some(Token::LParen));
15869        is_primary && is_key && is_lparen
15870    }
15871
15872    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15873    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15874    /// (mailrs round-5 G10).
15875    fn peek_table_level_unique_start(&self) -> bool {
15876        let cur = self.peek();
15877        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15878        if !is_unique {
15879            return false;
15880        }
15881        let n1 = self.tokens.get(self.pos + 1);
15882        // Plain `UNIQUE (…)`.
15883        if matches!(n1, Some(Token::LParen)) {
15884            return true;
15885        }
15886        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15887        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15888        if !is_nulls {
15889            return false;
15890        }
15891        let n2 = self.tokens.get(self.pos + 2);
15892        let n3 = self.tokens.get(self.pos + 3);
15893        let n4 = self.tokens.get(self.pos + 4);
15894        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15895        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15896            return true;
15897        }
15898        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15899        if matches!(n2, Some(Token::Not))
15900            && matches!(n3, Some(Token::Distinct))
15901            && matches!(n4, Some(Token::LParen))
15902        {
15903            return true;
15904        }
15905        false
15906    }
15907
15908    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15909        self.advance(); // PRIMARY
15910        self.advance(); // KEY
15911        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15912        // v7.39 (round 711) — the trailer's values are CARRIED now; round
15913        // 621 consumed and dropped them (the storing half of F08).
15914        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15915        Ok(crate::ast::TableConstraint::PrimaryKey {
15916            name: None,
15917            columns,
15918            deferrable,
15919            initially_deferred,
15920        })
15921    }
15922
15923    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15924        self.advance(); // UNIQUE
15925        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15926        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15927        // is `NULLS DISTINCT` per the SQL standard.
15928        let mut nulls_not_distinct = false;
15929        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15930            let n1 = self.tokens.get(self.pos + 1);
15931            let n2 = self.tokens.get(self.pos + 2);
15932            let is_not = matches!(n1, Some(Token::Not));
15933            let is_distinct = matches!(n2, Some(Token::Distinct));
15934            if is_not && is_distinct {
15935                self.advance(); // NULLS
15936                self.advance(); // NOT
15937                self.advance(); // DISTINCT
15938                nulls_not_distinct = true;
15939            } else if matches!(n1, Some(Token::Distinct)) {
15940                self.advance(); // NULLS
15941                self.advance(); // DISTINCT
15942            }
15943        }
15944        let columns = self.parse_paren_ident_list("UNIQUE")?;
15945        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15946        Ok(crate::ast::TableConstraint::Unique {
15947            name: None,
15948            columns,
15949            nulls_not_distinct,
15950            deferrable,
15951            initially_deferred,
15952            prefix_lengths: Vec::new(),
15953        })
15954    }
15955
15956    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15957    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15958    /// expression.
15959    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15960    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15961    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15962    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15963    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15964    /// commit: `NOT` starts no other suffix here, but reading both
15965    /// tokens before advancing keeps the caller's error message intact
15966    /// if someone writes `NOT NULL` by mistake.
15967    fn parse_not_valid_suffix(&mut self) -> bool {
15968        if !matches!(self.peek(), Token::Not) {
15969            return false;
15970        }
15971        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15972        {
15973            return false;
15974        }
15975        self.advance();
15976        self.advance();
15977        true
15978    }
15979
15980    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15981        self.advance(); // EXCLUDE
15982        // Optional `USING <method>`.
15983        let mut method = None;
15984        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15985            self.advance();
15986            method = Some(match self.advance() {
15987                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15988                other => {
15989                    return Err(self.err(alloc::format!(
15990                        "expected index method after USING, got {other:?}"
15991                    )));
15992                }
15993            });
15994        }
15995        if !matches!(self.peek(), Token::LParen) {
15996            return Err(self.err(alloc::format!(
15997                "expected '(' after EXCLUDE, got {:?}",
15998                self.peek()
15999            )));
16000        }
16001        self.advance();
16002        let mut elements: Vec<(String, String)> = Vec::new();
16003        loop {
16004            let col = match self.advance() {
16005                Token::Ident(s) | Token::QuotedIdent(s) => s,
16006                other => {
16007                    return Err(self.err(alloc::format!(
16008                        "expected column name in EXCLUDE, got {other:?}"
16009                    )));
16010                }
16011            };
16012            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16013                return Err(self.err(alloc::format!(
16014                    "expected WITH after EXCLUDE column, got {:?}",
16015                    self.peek()
16016                )));
16017            }
16018            self.advance();
16019            let op = match self.advance() {
16020                Token::InetOverlap => String::from("&&"),
16021                Token::Intersects => String::from("?#"),
16022                Token::IsBelow => String::from("<^"),
16023                Token::IsAbove => String::from(">^"),
16024                Token::PatternLt => String::from("~<~"),
16025                Token::PatternLtEq => String::from("~<=~"),
16026                Token::PatternGt => String::from("~>~"),
16027                Token::PatternGtEq => String::from("~>=~"),
16028                Token::TsMatchOld => String::from("@@@"),
16029                Token::Eq => String::from("="),
16030                Token::JsonContains => String::from("@>"),
16031                Token::JsonContainedBy => String::from("<@"),
16032                Token::OverLeft => String::from("&<"),
16033                Token::OverRight => String::from("&>"),
16034                other => {
16035                    return Err(self.err(alloc::format!(
16036                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
16037                    )));
16038                }
16039            };
16040            elements.push((col, op));
16041            if matches!(self.peek(), Token::Comma) {
16042                self.advance();
16043                continue;
16044            }
16045            break;
16046        }
16047        if !matches!(self.peek(), Token::RParen) {
16048            return Err(self.err(alloc::format!(
16049                "expected ')' to close EXCLUDE, got {:?}",
16050                self.peek()
16051            )));
16052        }
16053        self.advance();
16054        Ok(crate::ast::TableConstraint::Exclude {
16055            name: None,
16056            method,
16057            elements,
16058        })
16059    }
16060
16061    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
16062        self.advance(); // CHECK
16063        if !matches!(self.peek(), Token::LParen) {
16064            return Err(self.err(alloc::format!(
16065                "expected '(' after CHECK, got {:?}",
16066                self.peek()
16067            )));
16068        }
16069        self.advance();
16070        let expr = self.parse_expr(0)?;
16071        if !matches!(self.peek(), Token::RParen) {
16072            return Err(self.err(alloc::format!(
16073                "expected ')' to close CHECK predicate, got {:?}",
16074                self.peek()
16075            )));
16076        }
16077        self.advance();
16078        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
16079        // are no existing rows for PG to skip, so it rejects the suffix.
16080        Ok(crate::ast::TableConstraint::Check {
16081            name: None,
16082            expr,
16083            not_valid: false,
16084        })
16085    }
16086
16087    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
16088    fn peek_table_level_check_start(&self) -> bool {
16089        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
16090    }
16091
16092    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
16093    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
16094    /// on the dedicated FK path (`parse_table_level_fk` consumes its
16095    /// own CONSTRAINT prefix).
16096    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
16097        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16098            return None;
16099        }
16100        // tokens[pos+1] is the constraint name (any ident-like);
16101        // tokens[pos+2] is the kind keyword.
16102        match self.tokens.get(self.pos + 2) {
16103            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
16104                Some(NamedTableConstraintKind::Check)
16105            }
16106            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
16107                Some(NamedTableConstraintKind::Unique)
16108            }
16109            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
16110                Some(NamedTableConstraintKind::PrimaryKey)
16111            }
16112            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
16113                Some(NamedTableConstraintKind::Exclude)
16114            }
16115            _ => None,
16116        }
16117    }
16118
16119    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
16120        if !matches!(self.peek(), Token::LParen) {
16121            return Err(self.err(alloc::format!(
16122                "expected '(' after {ctx}, got {:?}",
16123                self.peek()
16124            )));
16125        }
16126        self.advance();
16127        let mut out = Vec::new();
16128        loop {
16129            out.push(self.expect_ident_like()?);
16130            match self.peek() {
16131                Token::Comma => {
16132                    self.advance();
16133                }
16134                Token::RParen => {
16135                    self.advance();
16136                    break;
16137                }
16138                other => {
16139                    return Err(self.err(alloc::format!(
16140                        "expected ',' or ')' in {ctx} list, got {other:?}"
16141                    )));
16142                }
16143            }
16144        }
16145        if out.is_empty() {
16146            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
16147        }
16148        Ok(out)
16149    }
16150
16151    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
16152    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
16153    /// table-level FK; a column def never starts with either keyword
16154    /// (column names are not in this reserved set).
16155    fn peek_constraint_or_fk_start(&self) -> bool {
16156        let is_constraint_kw = matches!(
16157            self.peek(),
16158            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
16159        );
16160        let is_foreign_kw = matches!(
16161            self.peek(),
16162            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
16163        );
16164        is_constraint_kw || is_foreign_kw
16165    }
16166
16167    /// v7.6.0 — parse a table-level FK clause:
16168    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
16169    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
16170    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
16171        let mut name: Option<String> = None;
16172        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16173            self.advance();
16174            name = Some(self.expect_ident_like()?);
16175        }
16176        // `FOREIGN`
16177        match self.advance() {
16178            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
16179            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
16180        }
16181        // `KEY`
16182        match self.advance() {
16183            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
16184            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
16185        }
16186        // `(col, col, ...)`
16187        if !matches!(self.peek(), Token::LParen) {
16188            return Err(self.err(format!(
16189                "expected '(' after FOREIGN KEY, got {:?}",
16190                self.peek()
16191            )));
16192        }
16193        self.advance();
16194        let mut columns = Vec::new();
16195        loop {
16196            columns.push(self.expect_ident_like()?);
16197            match self.peek() {
16198                Token::Comma => {
16199                    self.advance();
16200                }
16201                Token::RParen => {
16202                    self.advance();
16203                    break;
16204                }
16205                other => {
16206                    return Err(self.err(format!(
16207                        "expected ',' or ')' in FK column list, got {other:?}"
16208                    )));
16209                }
16210            }
16211        }
16212        if columns.is_empty() {
16213            return Err(self.err("FOREIGN KEY requires at least one column".into()));
16214        }
16215        let (
16216            parent_table,
16217            parent_columns,
16218            on_delete,
16219            on_update,
16220            match_type,
16221            deferrable,
16222            initially_deferred,
16223        ) = self.parse_references_tail(columns.len())?;
16224        Ok(ForeignKeyConstraint {
16225            name,
16226            columns,
16227            parent_table,
16228            parent_columns,
16229            on_delete,
16230            on_update,
16231            match_type,
16232            deferrable,
16233            initially_deferred,
16234        })
16235    }
16236
16237    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
16238    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
16239    /// the local column count, used to default the parent column
16240    /// list when omitted (SQL spec: parent's PK is implied).
16241    fn parse_references_tail(
16242        &mut self,
16243        expected_arity: usize,
16244    ) -> Result<
16245        (
16246            String,
16247            Vec<String>,
16248            FkAction,
16249            FkAction,
16250            crate::ast::MatchType,
16251            // v7.39 (round 288) — deferrable, initially_deferred.
16252            bool,
16253            bool,
16254        ),
16255        ParseError,
16256    > {
16257        match self.advance() {
16258            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
16259            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
16260        }
16261        let parent_table = self.expect_ident_like()?;
16262        let mut parent_columns: Vec<String> = Vec::new();
16263        if matches!(self.peek(), Token::LParen) {
16264            self.advance();
16265            loop {
16266                parent_columns.push(self.expect_ident_like()?);
16267                match self.peek() {
16268                    Token::Comma => {
16269                        self.advance();
16270                    }
16271                    Token::RParen => {
16272                        self.advance();
16273                        break;
16274                    }
16275                    other => {
16276                        return Err(self.err(format!(
16277                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
16278                        )));
16279                    }
16280                }
16281            }
16282        }
16283        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
16284            return Err(self.err(format!(
16285                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
16286                expected_arity,
16287                parent_columns.len()
16288            )));
16289        }
16290        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
16291        // it between the referenced column list and the ON / DEFERRABLE
16292        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
16293        // is skipped when any referencing column is NULL), so SIMPLE —
16294        // the default, and the only spelling pg_dump emits — is accepted
16295        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
16296        // mixed-NULL rule, which is not wired yet; reject them honestly
16297        // rather than silently applying SIMPLE (PG itself errors on
16298        // MATCH PARTIAL as "not yet implemented").
16299        let mut match_type = crate::ast::MatchType::Simple;
16300        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
16301            self.advance();
16302            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
16303            // SIMPLE / PARTIAL arrive as bare identifiers.
16304            let kind = match self.advance() {
16305                Token::Full => "FULL".to_string(),
16306                Token::Ident(s) => s.to_uppercase(),
16307                other => {
16308                    return Err(self.err(format!(
16309                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
16310                    )));
16311                }
16312            };
16313            match kind.as_str() {
16314                "SIMPLE" => {} // Default — match_type stays Simple.
16315                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
16316                // when ALL referencing columns are NULL; a mixed-NULL key errors.
16317                "FULL" => match_type = crate::ast::MatchType::Full,
16318                "PARTIAL" => {
16319                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
16320                }
16321                _ => {
16322                    return Err(self.err(format!(
16323                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
16324                    )));
16325                }
16326            }
16327        }
16328        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
16329        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
16330        // <action>` / `ON UPDATE <action>` in either order. PG /
16331        // pg_dump emits the timing clause AFTER the ON clauses
16332        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
16333        // but the SQL spec allows either order. We loop over
16334        // every possible trailer and dispatch on the next token,
16335        // stopping when nothing matches. Phase 3.1 changes the
16336        // bare DEFERRABLE form from hard-error to accept-as-
16337        // immediate; SPG is single-writer with no deferred-
16338        // constraint window so the runtime semantics are always
16339        // immediate even when INITIALLY DEFERRED is requested.
16340        // PG's default referential action (no ON DELETE / ON UPDATE
16341        // clause) is NO ACTION, not RESTRICT — the two enforce
16342        // identically in SPG (single-writer, no deferred window; see the
16343        // shared match arm in constraints.rs) but information_schema.
16344        // referential_constraints must report NO ACTION to match PG.
16345        let mut on_delete = FkAction::NoAction;
16346        let mut on_update = FkAction::NoAction;
16347        let mut seen_on_delete = false;
16348        let mut seen_on_update = false;
16349        let mut deferrable = false;
16350        let mut initially_deferred = false;
16351        loop {
16352            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
16353            let before = self.pos;
16354            let (d, idef) = self.consume_deferrable_clauses_timed()?;
16355            if self.pos != before {
16356                deferrable = d;
16357                initially_deferred = idef;
16358                continue;
16359            }
16360            // ON DELETE / ON UPDATE.
16361            if !matches!(self.peek(), Token::On) {
16362                break;
16363            }
16364            self.advance();
16365            let which = self.advance();
16366            let action = self.parse_fk_action()?;
16367            match which {
16368                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
16369                    if seen_on_delete {
16370                        return Err(self.err("ON DELETE specified twice".into()));
16371                    }
16372                    seen_on_delete = true;
16373                    on_delete = action;
16374                }
16375                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
16376                    if seen_on_update {
16377                        return Err(self.err("ON UPDATE specified twice".into()));
16378                    }
16379                    seen_on_update = true;
16380                    on_update = action;
16381                }
16382                other => {
16383                    return Err(
16384                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
16385                    );
16386                }
16387            }
16388        }
16389        Ok((
16390            parent_table,
16391            parent_columns,
16392            on_delete,
16393            on_update,
16394            match_type,
16395            deferrable,
16396            initially_deferred,
16397        ))
16398    }
16399
16400    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
16401    /// NO ACTION`.
16402    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
16403        match self.advance() {
16404            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
16405            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
16406            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
16407                Token::Null => Ok(FkAction::SetNull),
16408                Token::Default => Ok(FkAction::SetDefault),
16409                other => Err(self.err(format!(
16410                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
16411                ))),
16412            },
16413            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
16414                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
16415                other => Err(self.err(format!(
16416                    "expected ACTION after NO in FK action, got {other:?}"
16417                ))),
16418            },
16419            other => Err(self.err(format!(
16420                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
16421            ))),
16422        }
16423    }
16424
16425    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
16426    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
16427    fn consume_if_not_exists(&mut self) -> bool {
16428        // `IF` arrives as a bare Ident (we don't reserve it because it
16429        // also appears mid-expression in PG, though we don't support
16430        // those forms yet).
16431        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16432        if !looks_like_if {
16433            return false;
16434        }
16435        // Peek one ahead before committing: only consume IF when it's
16436        // actually `IF NOT EXISTS`.
16437        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
16438            return false;
16439        }
16440        if !matches!(
16441            self.tokens.get(self.pos + 2),
16442            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16443        ) {
16444            return false;
16445        }
16446        self.advance(); // IF
16447        self.advance(); // NOT
16448        self.advance(); // EXISTS
16449        true
16450    }
16451
16452    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
16453    /// Consumes IF EXISTS as a pair; returns false otherwise
16454    /// without consuming any tokens.
16455    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
16456    /// ENABLE/DISABLE/FORCE/NO FORCE.
16457    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
16458        for kw in ["row", "level", "security"] {
16459            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
16460            {
16461                return Err(self.err(alloc::format!(
16462                    "expected {} in ROW LEVEL SECURITY, got {:?}",
16463                    kw.to_ascii_uppercase(),
16464                    self.peek()
16465                )));
16466            }
16467            self.advance();
16468        }
16469        Ok(())
16470    }
16471
16472    fn consume_if_exists(&mut self) -> bool {
16473        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16474        if !looks_like_if {
16475            return false;
16476        }
16477        if !matches!(
16478            self.tokens.get(self.pos + 1),
16479            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16480        ) {
16481            return false;
16482        }
16483        self.advance(); // IF
16484        self.advance(); // EXISTS
16485        true
16486    }
16487
16488    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
16489    /// qualifiers after an index column ref. ASC / DESC are
16490    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
16491    /// We accept and discard them since single-column BTree
16492    /// stores rows in natural key order today.
16493    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
16494    /// ORDER BY key. Returns None when absent.
16495    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
16496        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16497            return Ok(None);
16498        }
16499        self.advance();
16500        match self.advance() {
16501            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
16502            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
16503            other => Err(self.err(alloc::format!(
16504                "expected FIRST or LAST after NULLS, got {other:?}"
16505            ))),
16506        }
16507    }
16508
16509    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
16510    /// rather than discarded.
16511    ///
16512    /// SPG's index does not scan in a direction — column ordering is
16513    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
16514    /// reproduction of the DDL, and dropping the clause meant
16515    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
16516    /// dump lost it, and a schema diff saw drift on every run.
16517    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
16518        let mut order = crate::ast::IndexColumnOrder::default();
16519        loop {
16520            match self.peek() {
16521                Token::Asc => {
16522                    self.advance();
16523                }
16524                Token::Desc => {
16525                    order.descending = true;
16526                    self.advance();
16527                }
16528                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
16529                    let look = self.tokens.get(self.pos + 1);
16530                    if matches!(
16531                        look,
16532                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
16533                            || k.eq_ignore_ascii_case("last")
16534                    ) {
16535                        self.advance();
16536                        order.nulls_first = Some(matches!(
16537                            self.advance(),
16538                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
16539                        ));
16540                    } else {
16541                        break;
16542                    }
16543                }
16544                _ => break,
16545            }
16546        }
16547        order
16548    }
16549
16550    fn parse_create_index_stmt_after_create(
16551        &mut self,
16552        is_unique: bool,
16553    ) -> Result<Statement, ParseError> {
16554        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
16555        debug_assert!(matches!(self.peek(), Token::Index));
16556        self.advance();
16557        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
16558        // SPG's CREATE INDEX is synchronous end-to-end today (real
16559        // CONCURRENTLY variant with restartable scans queues with
16560        // v7.39 indexes epic), so the modifier has no runtime effect
16561        // — same accept-and-no-op shape as v7.37.16.5 DETACH
16562        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
16563        // VIEW CONCURRENTLY.
16564        let mut concurrently = false;
16565        if matches!(
16566            self.peek(),
16567            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
16568        ) {
16569            self.advance();
16570            concurrently = true;
16571        }
16572        let if_not_exists = self.consume_if_not_exists();
16573        // v7.39 (read01 round 93) — the index name is optional (PG since
16574        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
16575        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
16576        // was given; leave it empty and the engine derives a PG-style
16577        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
16578        let name = if matches!(self.peek(), Token::On) {
16579            String::new()
16580        } else {
16581            self.expect_ident_like()?
16582        };
16583        if !matches!(self.peek(), Token::On) {
16584            return Err(self.err(format!(
16585                "expected ON after CREATE INDEX <name>, got {:?}",
16586                self.peek()
16587            )));
16588        }
16589        self.advance();
16590        let table = self.expect_ident_like()?;
16591        // Optional `USING <method>` — only recognised method in v2.0 is
16592        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
16593        // ident `using` (we don't promote it to a reserved keyword
16594        // because it isn't reserved anywhere else in our SQL surface).
16595        let mut method_name: Option<String> = None;
16596        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
16597            self.advance();
16598            let m = self.expect_ident_like()?;
16599            method_name = Some(m.to_ascii_lowercase());
16600            match m.to_ascii_lowercase().as_str() {
16601                "hnsw" => IndexMethod::Hnsw,
16602                "btree" => IndexMethod::BTree,
16603                "brin" => IndexMethod::Brin,
16604                // v7.12.3 — real GIN inverted index over `tsvector`.
16605                // v7.9.26b's `USING gin` → BTree silent fallback is
16606                // gone; the engine validates that the indexed column
16607                // is `tsvector` at CREATE INDEX time.
16608                "gin" => IndexMethod::Gin,
16609                // v7.9.26b — PG `pg_dump` emits `USING gist` /
16610                // `USING spgist` / `USING hash` for their built-in
16611                // AMs that SPG doesn't have a matching
16612                // implementation for; degrade to BTree on the
16613                // leading column so the schema loads + the index
16614                // catalogue stays consistent. Operator pays the
16615                // planner cost only for the queries that would have
16616                // used the specialised AM.
16617                "gist" | "spgist" | "hash" => IndexMethod::BTree,
16618                // v7.11.3 — pgvector ships both `ivfflat` and
16619                // `hnsw`. Customers shouldn't have to choose
16620                // their on-disk index method based on what SPG
16621                // implements; accept `ivfflat` as a synonym for
16622                // `hnsw` so PG schemas using either method drop
16623                // in. The vector distance op (`<->` / `<#>` /
16624                // `<=>`) at query time still picks the metric.
16625                "ivfflat" => IndexMethod::Hnsw,
16626                other => {
16627                    return Err(self.err(alloc::format!(
16628                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
16629                    )));
16630                }
16631            }
16632        } else {
16633            IndexMethod::BTree
16634        };
16635        if !matches!(self.peek(), Token::LParen) {
16636            return Err(self.err(format!(
16637                "expected '(' before indexed column, got {:?}",
16638                self.peek()
16639            )));
16640        }
16641        self.advance();
16642        // v6.8.2 — accept either a bare column ident (legacy) or
16643        // an expression `fn(col, …)` for expression indexes.
16644        // Distinguish by peeking the token *after* the current
16645        // ident: `ident )` is the legacy column-only path;
16646        // anything else triggers the Pratt expression parser.
16647        // (`advance()` uses `mem::replace` to nil out the current
16648        // slot, so we can't save+rewind cleanly — peek-ahead via
16649        // direct index avoids the mutation.)
16650        let mut opclass: Option<String> = None;
16651        let mut key_collation: Option<String> = None;
16652        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16653            // Single column with `)` immediately after — fast path.
16654            // v7.9.29 — also: bare column followed by `,` (the
16655            // multi-column form `(a, b, c)`). Without this branch
16656            // the leading ident gets pulled into `parse_expr`
16657            // which then sets `expression = Some(Column(a))` and
16658            // breaks Display round-trip on the multi-column shape.
16659            Token::Ident(s) | Token::QuotedIdent(s)
16660                if matches!(
16661                    self.tokens.get(self.pos + 1),
16662                    Some(Token::RParen | Token::Comma)
16663                ) =>
16664            {
16665                self.advance();
16666                (s, None)
16667            }
16668            // v7.9.22 — single column followed by a pgvector
16669            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16670            // v7.15.0 — capture the opclass instead of discarding
16671            // it so the engine can dispatch (e.g. `gin_trgm_ops`
16672            // → real trigram-shingle GIN over a TEXT column).
16673            // Vector/HNSW opclasses still take their distance
16674            // metric from the query operator (`<->` / `<#>` /
16675            // `<=>`), so for those callers the opclass stays
16676            // informational.
16677            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16678            // opclass: `(embedding public.vector_cosine_ops)`. Strip
16679            // the schema and dispatch on the bare opclass, the same
16680            // treatment table/type names get.
16681            Token::Ident(s) | Token::QuotedIdent(s)
16682                if matches!(
16683                    self.tokens.get(self.pos + 1),
16684                    Some(Token::Ident(_) | Token::QuotedIdent(_))
16685                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16686                    && matches!(
16687                        self.tokens.get(self.pos + 3),
16688                        Some(Token::Ident(op) | Token::QuotedIdent(op))
16689                            if is_vector_opclass_name(op)
16690                    ) =>
16691            {
16692                self.advance(); // column name
16693                self.advance(); // schema qualifier
16694                self.advance(); // dot
16695                let op_tok = self.advance();
16696                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16697                    opclass = Some(op.to_ascii_lowercase());
16698                }
16699                (s, None)
16700            }
16701            // r1038 — an operator class is recognised by its POSITION, not
16702            // by a list of names. It used to be `is_vector_opclass_name`,
16703            // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16704            // sentori's migration wrote — was a syntax error while
16705            // `USING gin (doc)` parsed. Anything sitting between a column
16706            // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16707            // two bare identifiers in a row are not valid there otherwise.
16708            Token::Ident(s) | Token::QuotedIdent(s)
16709                if matches!(
16710                    self.tokens.get(self.pos + 1),
16711                    Some(Token::Ident(op) | Token::QuotedIdent(op))
16712                        if is_vector_opclass_name(op) || Self::opclass_position_follows(
16713                            self.tokens.get(self.pos + 2)
16714                        )
16715                ) =>
16716            {
16717                self.advance(); // column name
16718                // Capture the opclass token, lower-cased for
16719                // case-insensitive engine dispatch.
16720                let op_tok = self.advance();
16721                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16722                    opclass = Some(op.to_ascii_lowercase());
16723                }
16724                (s, None)
16725            }
16726            Token::Ident(_) | Token::QuotedIdent(_) => {
16727                // v7.39 (round 538) — an explicit COLLATE on the key,
16728                // read by LOOKAHEAD because `parse_expr` absorbs the
16729                // clause as a no-op (SPG orders text by bytes, which is
16730                // the C collation, so it changes nothing to honour). PG
16731                // still PRINTS it: an explicitly written `"C"` and the
16732                // collation a column inherits are different collation
16733                // OBJECTS even where they sort identically, which is why
16734                // `(a COLLATE "C")` shows on a C-collation database too.
16735                if matches!(
16736                    self.tokens.get(self.pos + 1),
16737                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16738                ) {
16739                    key_collation = match self.tokens.get(self.pos + 2) {
16740                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16741                            Some(n.clone())
16742                        }
16743                        _ => None,
16744                    };
16745                }
16746                // v7.39.2 — the clause is read by the LOOKAHEAD above and
16747                // belongs to the KEY, not to the expression. Since
16748                // `COLLATE` became a node, letting `parse_expr` build one
16749                // here put the collation in twice and the key deparsed as
16750                // `(c COLLATE "C" COLLATE "C")`. The ORDER-BY-key channel
16751                // is the same idea and already exists, so this borrows it:
16752                // absorb into the side channel, and the key's own
16753                // lookahead is what carries it.
16754                // v7.39.2 — and the key can only CARRY the byte-order
16755                // spellings. Absorbing into the side channel accepts any
16756                // name, so suppressing the node here without this check
16757                // silently accepted `(name COLLATE "en_US")`, which SPG's
16758                // index cannot honour — a refusal that was doing real
16759                // work, removed by the suppression and put back here.
16760                if let Some(name) = &key_collation {
16761                    let lc = name.to_ascii_lowercase();
16762                    let byte_order = matches!(
16763                        lc.as_str(),
16764                        "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
16765                    );
16766                    let mysql_ok = self.mysql_dialect
16767                        && (lc.ends_with("_ci")
16768                            || lc.ends_with("_bin")
16769                            || lc == "binary"
16770                            || matches!(lc.as_str(), "case_insensitive" | "nocase"));
16771                    if !byte_order && !mysql_ok {
16772                        return Err(self.err(alloc::format!(
16773                            "COLLATE {name:?} is not supported in this position: an index \
16774                             key carries the byte-order spellings only. Declare it on the \
16775                             column (`x text COLLATE {name:?}`) instead"
16776                        )));
16777                    }
16778                }
16779                let saved_key_ctx = self.in_order_by_key;
16780                self.in_order_by_key = true;
16781                let key_expr = self.parse_expr(0);
16782                self.in_order_by_key = saved_key_ctx;
16783                let key_expr = key_expr?;
16784                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16785                    self.err("expression index key must reference at least one column".into())
16786                })?;
16787                (primary, Some(key_expr))
16788            }
16789            // v7.37.43-T4 — parenthesised expression index key
16790            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16791            // PG's CREATE INDEX requires the expression to be in
16792            // its own parens to disambiguate function calls from
16793            // column lists, so this `LParen` is the inner open-paren
16794            // of an expression key. parse_expr handles the recursive
16795            // descent and consumes the matching `RParen`.
16796            Token::LParen => {
16797                let key_expr = self.parse_expr(0)?;
16798                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16799                    self.err("expression index key must reference at least one column".into())
16800                })?;
16801                (primary, Some(key_expr))
16802            }
16803            other => {
16804                return Err(self.err(format!(
16805                    "expected column ident or expression, got {other:?}"
16806                )));
16807            }
16808        };
16809        // v7.9.14 — accept extra comma-separated columns inside
16810        // the index key parens (`CREATE INDEX … (a, b, c)`).
16811        // mailrs F2.
16812        //
16813        // v7.39.11 — each extra column's `ASC` / `DESC` / `NULLS FIRST`
16814        // / `NULLS LAST` is KEPT. It used to be parsed and dropped on
16815        // the floor, so `CREATE INDEX i ON t (a, b DESC)` read back from
16816        // `pg_get_indexdef` as `(a, b)`: a dump lost the clause and a
16817        // schema diff saw drift on every run. Reported by sentori
16818        // against 7.39.10, and the same defect round 537 fixed for the
16819        // LEADING column, in the loop right beside it.
16820        let mut extra_columns: Vec<String> = Vec::new();
16821        let mut extra_orders: Vec<crate::ast::IndexColumnOrder> = Vec::new();
16822        // The leading column may also have ASC/DESC after it — and that
16823        // one is the column SPG indexes, so its clause is kept.
16824        let key_order = self.consume_optional_index_column_qualifiers();
16825        while matches!(self.peek(), Token::Comma) {
16826            self.advance();
16827            let extra = self.expect_ident_like()?;
16828            extra_orders.push(self.consume_optional_index_column_qualifiers());
16829            extra_columns.push(extra);
16830        }
16831        if !matches!(self.peek(), Token::RParen) {
16832            return Err(self.err(format!(
16833                "expected ')' after indexed column / expression, got {:?}",
16834                self.peek()
16835            )));
16836        }
16837        self.advance();
16838        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16839        // index-only-scan annotation. Bare ident (not a reserved
16840        // keyword) so we test by case-insensitive string match.
16841        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16842        {
16843            self.advance();
16844            if !matches!(self.peek(), Token::LParen) {
16845                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16846            }
16847            self.advance();
16848            let mut cols = Vec::new();
16849            loop {
16850                cols.push(self.expect_ident_like()?);
16851                match self.peek() {
16852                    Token::Comma => {
16853                        self.advance();
16854                    }
16855                    Token::RParen => {
16856                        self.advance();
16857                        break;
16858                    }
16859                    other => {
16860                        return Err(self.err(format!(
16861                            "expected ',' or ')' in INCLUDE list, got {other:?}"
16862                        )));
16863                    }
16864                }
16865            }
16866            cols
16867        } else {
16868            Vec::new()
16869        };
16870        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16871        // storage parameters. pgvector emits `WITH (lists = N)` for
16872        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16873        // SPG's HNSW picks its own parameters today (tunable via
16874        // env vars), so the WITH clause is informational and dropped.
16875        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16876            self.advance();
16877            if !matches!(self.peek(), Token::LParen) {
16878                return Err(self.err(format!(
16879                    "expected '(' after WITH in CREATE INDEX, got {:?}",
16880                    self.peek()
16881                )));
16882            }
16883            self.advance();
16884            loop {
16885                if matches!(self.peek(), Token::RParen) {
16886                    self.advance();
16887                    break;
16888                }
16889                // Drain `key = value` or bare `key` tokens.
16890                let _ = self.advance(); // key
16891                if matches!(self.peek(), Token::Eq) {
16892                    self.advance();
16893                    let _ = self.advance(); // value (int / string / ident)
16894                }
16895                match self.peek() {
16896                    Token::Comma => {
16897                        self.advance();
16898                    }
16899                    Token::RParen => {
16900                        self.advance();
16901                        break;
16902                    }
16903                    other => {
16904                        return Err(self.err(format!(
16905                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
16906                        )));
16907                    }
16908                }
16909            }
16910        }
16911        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16912        // which sits between the key list and the WHERE clause.
16913        let mut nulls_not_distinct = false;
16914        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16915            let n1 = self.tokens.get(self.pos + 1);
16916            let n2 = self.tokens.get(self.pos + 2);
16917            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16918                self.advance(); // NULLS
16919                self.advance(); // NOT
16920                self.advance(); // DISTINCT
16921                nulls_not_distinct = true;
16922            } else if matches!(n1, Some(Token::Distinct)) {
16923                self.advance(); // NULLS
16924                self.advance(); // DISTINCT
16925            }
16926        }
16927        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16928        let partial_predicate = if matches!(self.peek(), Token::Where) {
16929            self.advance();
16930            Some(self.parse_expr(0)?)
16931        } else {
16932            None
16933        };
16934        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16935        // sense: uniqueness over an ANN structure has no clean
16936        // semantics. Reject early. (BRIN UNIQUE is similarly
16937        // meaningless — block both.)
16938        if is_unique && !matches!(method, IndexMethod::BTree) {
16939            return Err(self.err(alloc::format!(
16940                "UNIQUE is only supported on BTree indexes, got USING {:?}",
16941                method
16942            )));
16943        }
16944        Ok(Statement::CreateIndex(CreateIndexStatement {
16945            concurrently,
16946            name,
16947            key_order,
16948            key_collation,
16949            table,
16950            column,
16951            nulls_not_distinct,
16952            method,
16953            if_not_exists,
16954            included_columns,
16955            partial_predicate,
16956            extra_columns: extra_columns.clone(),
16957            extra_orders: extra_orders.clone(),
16958            expression,
16959            is_unique,
16960            opclass,
16961            method_name,
16962        }))
16963    }
16964
16965    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16966    /// column-level `REFERENCES ...` clause. The trailing FK is
16967    /// normalised into table-level shape (single-element columns +
16968    /// parent_columns) so the engine sees one uniform constraint list.
16969    fn parse_column_def_with_fk(
16970        &mut self,
16971    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16972        let col = self.parse_column_def()?;
16973        // v7.39 (round 308, V29) — an explicitly named inline FK:
16974        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16975        // loop leaves this spelling intact precisely so the name can be
16976        // kept here; PG reports it in violation messages and matches it
16977        // in `SET CONSTRAINTS`.
16978        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16979        {
16980            self.advance();
16981            Some(self.expect_ident_like()?)
16982        } else {
16983            None
16984        };
16985        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16986        let inline_references = matches!(
16987            self.peek(),
16988            Token::Ident(s) if s.eq_ignore_ascii_case("references")
16989        );
16990        if !inline_references {
16991            return Ok((col, None));
16992        }
16993        let (
16994            parent_table,
16995            parent_columns,
16996            on_delete,
16997            on_update,
16998            match_type,
16999            deferrable,
17000            initially_deferred,
17001        ) = self.parse_references_tail(1)?;
17002        let fk = ForeignKeyConstraint {
17003            name: declared_name,
17004            columns: vec![col.name.clone()],
17005            parent_table,
17006            parent_columns,
17007            on_delete,
17008            on_update,
17009            match_type,
17010            deferrable,
17011            initially_deferred,
17012        };
17013        Ok((col, Some(fk)))
17014    }
17015
17016    /// v7.13.0 — parse a column type (consuming the type ident and
17017    /// any trailing parameters / `[]`), without surrounding column
17018    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
17019    /// Returns the resolved `ColumnTypeName` plus implied
17020    /// `(auto_increment, not_null)` flags from PG SERIAL family
17021    /// shorthands — callers that don't expect those (ALTER COLUMN
17022    /// TYPE) can discard them.
17023    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
17024        let (ty, _, _, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
17025        Ok(ty)
17026    }
17027
17028    #[allow(clippy::type_complexity)]
17029    fn parse_type_with_implied_flags(
17030        &mut self,
17031    ) -> Result<
17032        (
17033            ColumnTypeName,
17034            bool,
17035            bool,
17036            Option<String>,
17037            Collation,
17038            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
17039            bool,
17040            // v7.39 (round 676) — the collation NAME as written, which the
17041            // `Collation` enum above cannot carry.
17042            Option<String>,
17043            bool,
17044            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
17045            // list captured at type-parse time. None for all
17046            // non-ENUM types.
17047            Option<Vec<String>>,
17048            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
17049            // list. Distinct from ENUM (subset semantics).
17050            Option<Vec<String>>,
17051            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
17052            // width, lost when the type collapses to SmallInt / Int.
17053            Option<MysqlIntWidth>,
17054            // v7.39 (round 424) — declared fractional-seconds precision of a
17055            // MySQL temporal column (bare spelling = 0). None under PG.
17056            Option<u8>,
17057            // v7.39.2 — written `TIMESTAMP` rather than `DATETIME`. The
17058            // two are different types on MySQL and SPG stores both as
17059            // `Timestamp`, so the spelling has to travel separately or
17060            // a dump silently rewrites the column.
17061            bool,
17062            // v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair. Not a
17063            // display hint: it rounds on write.
17064            Option<(u8, u8)>,
17065        ),
17066        ParseError,
17067    > {
17068        let mut ty_ident = match self.advance() {
17069            Token::Ident(s) => s,
17070            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
17071            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
17072            // '<span>'` literal grammar. As a column type it lands
17073            // here directly; downstream resolution still uses the
17074            // canonical lowercase string.
17075            Token::Interval => "interval".to_string(),
17076            other => {
17077                return Err(ParseError {
17078                    message: format!("expected column type, got {other:?}"),
17079                    token_pos: self.consumed_pos(),
17080                });
17081            }
17082        };
17083        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
17084        // pg_dump qualifies extension types (`public.vector(1024)`).
17085        // SPG is single-namespace; drop the schema and resolve the
17086        // bare type — same treatment table names already get.
17087        while matches!(self.peek(), Token::Dot) {
17088            self.advance();
17089            ty_ident = self.expect_ident_like()?;
17090        }
17091        let mut implied_auto_increment = false;
17092        let mut implied_not_null = false;
17093        let mut user_type_ref: Option<String> = None;
17094        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
17095        // value list, captured here and bubbled up through the
17096        // ColumnDef so the engine can attach it to the column
17097        // schema (and validate INSERT cells against it).
17098        let mut inline_enum_variants: Option<Vec<String>> = None;
17099        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
17100        let mut inline_set_variants: Option<Vec<String>> = None;
17101        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
17102        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
17103        // collapses to SmallInt / Int. Only under the MySQL dialect.
17104        let mut mysql_int_width: Option<MysqlIntWidth> = None;
17105        // v7.39 (round 424) — the declared fractional-seconds precision of a
17106        // MySQL temporal column. Set by the temporal arms below; stays None
17107        // for PG (whose temporal columns keep full microseconds).
17108        let mut mysql_fsp: Option<u8> = None;
17109        let mut mysql_declared_timestamp = false;
17110        let mut mysql_float_md: Option<(u8, u8)> = None;
17111        let mut ty = match ty_ident.as_str() {
17112            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
17113            "smallserial" | "serial2" => {
17114                implied_auto_increment = true;
17115                implied_not_null = true;
17116                ColumnTypeName::SmallInt
17117            }
17118            "serial" | "serial4" => {
17119                implied_auto_increment = true;
17120                implied_not_null = true;
17121                ColumnTypeName::Int
17122            }
17123            "bigserial" | "serial8" => {
17124                implied_auto_increment = true;
17125                implied_not_null = true;
17126                ColumnTypeName::BigInt
17127            }
17128            // MySQL flavours we accept by aliasing to the closest SPG
17129            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
17130            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
17131            // 24-bit) → INT. UNSIGNED modifiers are consumed below
17132            // without semantic effect.
17133            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
17134            // PG's internal type names; pg_dump and hand-written PG schemas
17135            // use them interchangeably with smallint / int / bigint (the cast
17136            // path already accepted them, only the column grammar didn't).
17137            "smallint" | "int2" => {
17138                // v7.14.0 — MySQL display-width on integers
17139                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
17140                // parenthesised number is purely cosmetic — it
17141                // doesn't change storage. Accept + discard.
17142                self.consume_optional_paren_size();
17143                ColumnTypeName::SmallInt
17144            }
17145            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
17146            // canonical encoding for BOOLEAN. Every MySQL driver
17147            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
17148            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
17149            // 4.3 SPG classified TINYINT(1) as SmallInt, which
17150            // gave the customer i16-shaped values where the app
17151            // expected bool — a Tier-A silent type drift on
17152            // mysqldump restores. Now: `TINYINT(1)` → Bool;
17153            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
17154            // stay SmallInt (the legacy width-agnostic path).
17155            "tinyint" => {
17156                let width = self.peek_optional_paren_size_value();
17157                self.consume_optional_paren_size();
17158                if width == Some(1) {
17159                    ColumnTypeName::Bool
17160                } else {
17161                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
17162                    // lost width so the write path can enforce -128..127.
17163                    if self.mysql_dialect {
17164                        mysql_int_width = Some(MysqlIntWidth::Tiny);
17165                    }
17166                    ColumnTypeName::SmallInt
17167                }
17168            }
17169            "mediumint" => {
17170                self.consume_optional_paren_size();
17171                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
17172                if self.mysql_dialect {
17173                    mysql_int_width = Some(MysqlIntWidth::Medium);
17174                }
17175                ColumnTypeName::Int
17176            }
17177            "int" | "integer" | "int4" => {
17178                self.consume_optional_paren_size();
17179                ColumnTypeName::Int
17180            }
17181            "bigint" | "int8" => {
17182                self.consume_optional_paren_size();
17183                ColumnTypeName::BigInt
17184            }
17185            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
17186            // (mailrs round-5 G6). Consume the optional `PRECISION`
17187            // tail when the type keyword was `double` / `DOUBLE`.
17188            //
17189            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
17190            // FLOAT". `FLOAT(p)` picks the width the way PG does:
17191            // p in 1..=24 is real, 25..=53 is double precision, and
17192            // anything else is an error.
17193            "float" | "double" | "real" => {
17194                if ty_ident.eq_ignore_ascii_case("double")
17195                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
17196                {
17197                    self.advance();
17198                }
17199                if ty_ident.eq_ignore_ascii_case("real") {
17200                    // v7.39 (round 274) — the two dialects genuinely
17201                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
17202                    // synonym for DOUBLE (8-byte). Round 269 made REAL
17203                    // 32-bit globally and thereby narrowed the stored
17204                    // precision of every MySQL REAL column.
17205                    if self.mysql_dialect {
17206                        ColumnTypeName::Float
17207                    } else {
17208                        ColumnTypeName::Real
17209                    }
17210                } else if self.mysql_dialect
17211                    && matches!(self.peek(), Token::LParen)
17212                    && self.peek_paren_has_comma()
17213                {
17214                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
17215                    // display form (`FLOAT(10,2)`), which PG has no
17216                    // equivalent of. It was `syntax error at or near ","`,
17217                    // so the whole CREATE failed.
17218                    //
17219                    // v7.39.2 — the guard said `float` while the comment
17220                    // said both, so `DOUBLE(10,2)` — which every legacy
17221                    // MySQL schema uses for money — still failed the
17222                    // whole CREATE with `syntax error at or near "("`.
17223                    // Measured on 9.7.2: both forms are accepted, and the
17224                    // digits are NOT a display hint, they round on write
17225                    // (3.14159265358979 into either stores 3.14). The
17226                    // rounding is recorded as a residual; accepting the
17227                    // syntax and keeping the width is the half this
17228                    // change makes.
17229                    // v7.39.3 — keep the pair. The digits are not a
17230                    // display hint: MySQL 9.7.2 ROUNDS on write and
17231                    // refuses a value wider than `m` (errno 1264), so a
17232                    // column declared for money held more precision here
17233                    // than its schema said.
17234                    let (m, d) = self.parse_optional_numeric_params()?;
17235                    mysql_float_md = Some((
17236                        u8::try_from(m).unwrap_or(u8::MAX),
17237                        u8::try_from(d.max(0)).unwrap_or(u8::MAX),
17238                    ));
17239                    if ty_ident.eq_ignore_ascii_case("float") {
17240                        ColumnTypeName::Real
17241                    } else {
17242                        ColumnTypeName::Float
17243                    }
17244                } else if ty_ident.eq_ignore_ascii_case("float")
17245                    && matches!(self.peek(), Token::LParen)
17246                {
17247                    // PG words the two bounds differently, and
17248                    // parse_paren_size already rejects a zero.
17249                    let p = self.parse_paren_size("FLOAT")?;
17250                    if p > 53 {
17251                        return Err(self.err(String::from(
17252                            "precision for type float must be less than 54 bits",
17253                        )));
17254                    }
17255                    if p <= 24 {
17256                        ColumnTypeName::Real
17257                    } else {
17258                        ColumnTypeName::Float
17259                    }
17260                } else if ty_ident.eq_ignore_ascii_case("float") && self.mysql_dialect {
17261                    // v7.39.2 — MySQL's bare FLOAT is FOUR bytes; PG's is
17262                    // eight (it is `float8`'s spelling there). SPG used
17263                    // PG's for both, so a MySQL FLOAT column silently
17264                    // kept more precision than MySQL does — measured,
17265                    // 3.14159265358979 comes back as 3.14159 there and
17266                    // came back whole here — and reported itself as
17267                    // `double` to every reflection.
17268                    //
17269                    // This is the mirror of the REAL split above: the
17270                    // two dialects disagree about which spelling means
17271                    // which width, and one of them was already honoured.
17272                    ColumnTypeName::Real
17273                } else {
17274                    ColumnTypeName::Float
17275                }
17276            }
17277            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
17278            "float4" => ColumnTypeName::Real,
17279            "float8" => ColumnTypeName::Float,
17280            "text" => ColumnTypeName::Text,
17281            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
17282            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
17283            // real MySQL schema and NONE of them existed: the CREATE
17284            // failed outright with `type "blob" does not exist`, so the
17285            // table was never made. The sizes differ only in MySQL's
17286            // maximum length, which SPG does not cap, so they collapse
17287            // onto TEXT and BYTEA the way the unsized spellings do.
17288            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
17289            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
17290            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
17291            // enforce, consumed so the declaration parses.
17292            "varbinary" | "binary" => {
17293                self.consume_optional_paren_size();
17294                ColumnTypeName::Bytes
17295            }
17296            "name" => ColumnTypeName::Name,
17297            "xid" => ColumnTypeName::Xid,
17298            "oid" => ColumnTypeName::Oid,
17299            "xid8" => ColumnTypeName::Xid8,
17300            "bool" | "boolean" => ColumnTypeName::Bool,
17301            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
17302            // an unbounded `character varying`, which the arm below has always
17303            // read as text. Only the short spelling demanded a length, so
17304            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
17305            // there is — failed on `VARCHAR type requires (N)` while the long
17306            // spelling of the same thing was accepted. The same asymmetry
17307            // round 613 closed on the CAST side, here on the DDL side.
17308            "varchar" => {
17309                if matches!(self.peek(), Token::LParen) {
17310                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
17311                } else {
17312                    ColumnTypeName::Text
17313                }
17314            }
17315            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
17316            // `character` below (SQL standard).
17317            "char" => {
17318                if matches!(self.peek(), Token::LParen) {
17319                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
17320                } else {
17321                    ColumnTypeName::Char(1)
17322                }
17323            }
17324            // pg_dump's canonical spellings: `character varying(n)` = varchar,
17325            // `character(n)` = char, bare `character` = char(1). Unbounded
17326            // `character varying` maps to text.
17327            "character" => {
17328                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
17329                    self.advance();
17330                    if matches!(self.peek(), Token::LParen) {
17331                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
17332                    } else {
17333                        ColumnTypeName::Text
17334                    }
17335                } else if matches!(self.peek(), Token::LParen) {
17336                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
17337                } else {
17338                    ColumnTypeName::Char(1)
17339                }
17340            }
17341            "vector" => {
17342                let dim = self.parse_paren_size("VECTOR")?;
17343                let encoding = self.parse_optional_vector_encoding()?;
17344                ColumnTypeName::Vector { dim, encoding }
17345            }
17346            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
17347            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
17348            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
17349            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
17350            // DECIMAL(10,2))` — how nearly every money column is written,
17351            // in either dialect — was a syntax error and the table was
17352            // never created. `FIXED` is MySQL's alias alone, so it is
17353            // taken only in that dialect.
17354            "numeric" | "decimal" | "dec" => {
17355                let (precision, scale) = self.parse_optional_numeric_params()?;
17356                ColumnTypeName::Numeric(precision, scale)
17357            }
17358            "fixed" if self.mysql_dialect => {
17359                let (precision, scale) = self.parse_optional_numeric_params()?;
17360                ColumnTypeName::Numeric(precision, scale)
17361            }
17362            "date" => ColumnTypeName::Date,
17363            // MySQL's `DATETIME` is the same domain as standard
17364            // `TIMESTAMP` — accept both spellings.
17365            "timestamp" | "datetime" => {
17366                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
17367                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
17368                // TIME ZONE` clause, so consume it first.
17369                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
17370                // (it truncates on write and pads on render), so capture it;
17371                // a bare spelling means precision 0 there. PG stores µs always
17372                // and keeps `None`.
17373                let n = self.take_optional_paren_size();
17374                if self.mysql_dialect {
17375                    mysql_fsp = Some(n.unwrap_or(0).min(6));
17376                    // v7.39.2 — remember WHICH spelling was written.
17377                    // MySQL and MariaDB keep `timestamp` and `datetime`
17378                    // apart everywhere a client can read the type back,
17379                    // and SPG reported `datetime` for both — so a dump
17380                    // and reload silently changed the column's declared
17381                    // type, and MySQL's TIMESTAMP is not DATETIME (a
17382                    // different range, and UTC conversion on the way in
17383                    // and out).
17384                    mysql_declared_timestamp = ty_ident.eq_ignore_ascii_case("timestamp");
17385                }
17386                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
17387                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
17388                // the full form. SPG canonicalises:
17389                //   - WITH TIME ZONE    → Timestamptz
17390                //   - WITHOUT TIME ZONE → Timestamp
17391                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17392                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17393                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17394                {
17395                    self.advance(); // WITH
17396                    self.advance(); // TIME
17397                    self.advance(); // ZONE
17398                    ColumnTypeName::Timestamptz
17399                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17400                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17401                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17402                {
17403                    self.advance(); // WITHOUT
17404                    self.advance(); // TIME
17405                    self.advance(); // ZONE
17406                    ColumnTypeName::Timestamp
17407                } else {
17408                    // A second `(precision)` cannot legally follow, but the
17409                    // old grammar tolerated it; keep that tolerance.
17410                    self.consume_optional_paren_size();
17411                    ColumnTypeName::Timestamp
17412                }
17413            }
17414            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
17415            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
17416            // only PG-wire OID differs.
17417            "timestamptz" => {
17418                self.consume_optional_paren_size();
17419                ColumnTypeName::Timestamptz
17420            }
17421            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
17422            // validation. We accept the JSONB spelling too because
17423            // most PG clients default to it; SPG doesn't distinguish
17424            // the two (no path-operator perf advantage to model).
17425            "json" => ColumnTypeName::Json,
17426            "jsonb" => ColumnTypeName::Jsonb,
17427            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
17428            // surface here. Same storage shape; mapping happens at
17429            // the engine side via the ColumnTypeName → DataType
17430            // resolver. Literal forms are handled at coerce_value
17431            // time so the lexer stays untouched.
17432            "bytea" | "bytes" => ColumnTypeName::Bytes,
17433            // v7.17.0 Phase 7 — PG network address types
17434            // v7.17.0 had a Text-backed fallback here for
17435            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
17436            // each to a first-class type; the keywords are
17437            // bound below in the ζ-A block.
17438            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
17439            // The actual `to_tsvector` / `@@` / `ts_rank` surface
17440            // arrives in v7.12.1+; the type itself loads here so
17441            // mailrs's `scripts/init-schema.sql` runs unmodified.
17442            "tsvector" => ColumnTypeName::TsVector,
17443            "tsquery" => ColumnTypeName::TsQuery,
17444            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
17445            // surface for Django / Rails / Hibernate's default
17446            // PK pattern.
17447            "uuid" => ColumnTypeName::Uuid,
17448            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
17449            // Storage = three-field {months, days, micros}, catalog
17450            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
17451            // line `INTERVAL` was parser-rejected at CREATE TABLE.
17452            "interval" => {
17453                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
17454                // SECOND` and an optional `(p)` precision. SPG stores the full
17455                // {months,days,micros}; consume + ignore the qualifier/precision.
17456                while matches!(self.peek(), Token::To)
17457                    || matches!(self.peek(), Token::Ident(s) if matches!(
17458                        s.to_ascii_lowercase().as_str(),
17459                        "year" | "month" | "day" | "hour" | "minute" | "second"
17460                    ))
17461                {
17462                    self.advance();
17463                }
17464                self.consume_optional_paren_size();
17465                ColumnTypeName::Interval
17466            }
17467            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
17468            // i64 microseconds since 00:00:00. Wire OID 1083.
17469            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
17470            "time" => {
17471                // v7.39 (round 424) — MySQL TIME carries a semantic
17472                // fractional-seconds precision, bare meaning 0.
17473                let n = self.take_optional_paren_size();
17474                if self.mysql_dialect {
17475                    mysql_fsp = Some(n.unwrap_or(0).min(6));
17476                }
17477                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17478                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17479                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17480                {
17481                    self.advance();
17482                    self.advance();
17483                    self.advance();
17484                    ColumnTypeName::TimeTz
17485                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17486                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17487                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17488                {
17489                    self.advance();
17490                    self.advance();
17491                    self.advance();
17492                    ColumnTypeName::Time
17493                } else {
17494                    ColumnTypeName::Time
17495                }
17496            }
17497            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
17498            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
17499            "year" => ColumnTypeName::Year,
17500            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
17501            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
17502            "timetz" => ColumnTypeName::TimeTz,
17503            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
17504            // Wire OID 790.
17505            "money" => ColumnTypeName::Money,
17506            // v7.17.0 Phase 3.P0-38 — PG range types.
17507            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
17508            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
17509            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
17510            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
17511            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
17512            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
17513            // v7.37.5 δ — PG 14+ multirange keywords.
17514            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
17515            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
17516            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
17517            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
17518            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
17519            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
17520            // v7.37.5 ε — PG geometry scalar keywords.
17521            "point" => ColumnTypeName::Point,
17522            "lseg" => ColumnTypeName::Lseg,
17523            "path" => ColumnTypeName::Path,
17524            "box" => ColumnTypeName::PgBox,
17525            "polygon" => ColumnTypeName::Polygon,
17526            "line" => ColumnTypeName::Line,
17527            "circle" => ColumnTypeName::Circle,
17528            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
17529            "inet" => ColumnTypeName::Inet,
17530            "cidr" => ColumnTypeName::Cidr,
17531            "macaddr" => ColumnTypeName::Macaddr,
17532            "macaddr8" => ColumnTypeName::Macaddr8,
17533            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
17534            // width in the value, so the optional `(N)` typmod is accepted and
17535            // ignored (the column stores whatever width it's given).
17536            "bit" => {
17537                let varying = matches!(
17538                    self.peek(),
17539                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
17540                );
17541                if varying {
17542                    self.advance();
17543                }
17544                // v7.39 (round 281) — the length used to be parsed and
17545                // dropped, so `bit(3)` accepted a five-bit string.
17546                let n = if matches!(self.peek(), Token::LParen) {
17547                    self.parse_paren_size("BIT")?
17548                } else {
17549                    0
17550                };
17551                if varying {
17552                    ColumnTypeName::BitVarying(n)
17553                } else {
17554                    ColumnTypeName::Bit(n)
17555                }
17556            }
17557            "varbit" => {
17558                let n = if matches!(self.peek(), Token::LParen) {
17559                    self.parse_paren_size("VARBIT")?
17560                } else {
17561                    0
17562                };
17563                ColumnTypeName::BitVarying(n)
17564            }
17565            "xml" => ColumnTypeName::Xml,
17566            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
17567            "hstore" => ColumnTypeName::Hstore,
17568            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
17569            // `ENUM('a','b','c')`. Storage is TEXT; the value
17570            // list lands on `inline_enum_variants` for the
17571            // engine to validate INSERT cells against. Empty
17572            // value list is a parse error (matches MySQL).
17573            "enum" => {
17574                // Expect the opening `(`.
17575                if !matches!(self.peek(), Token::LParen) {
17576                    return Err(self.err(alloc::format!(
17577                        "expected '(' after ENUM, got {:?}",
17578                        self.peek()
17579                    )));
17580                }
17581                self.advance();
17582                let mut variants: Vec<String> = Vec::new();
17583                loop {
17584                    match self.advance() {
17585                        Token::String(s) => variants.push(s),
17586                        other => {
17587                            return Err(self.err(alloc::format!(
17588                                "ENUM(...) expects string literal variants, got {other:?}"
17589                            )));
17590                        }
17591                    }
17592                    match self.peek() {
17593                        Token::Comma => {
17594                            self.advance();
17595                            continue;
17596                        }
17597                        Token::RParen => {
17598                            self.advance();
17599                            break;
17600                        }
17601                        other => {
17602                            return Err(self.err(alloc::format!(
17603                                "expected ',' or ')' in ENUM(...), got {other:?}"
17604                            )));
17605                        }
17606                    }
17607                }
17608                if variants.is_empty() {
17609                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
17610                }
17611                inline_enum_variants = Some(variants);
17612                // Storage is plain TEXT; the variant list lives on
17613                // the ColumnSchema side.
17614                ColumnTypeName::Text
17615            }
17616            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
17617            // `SET('a','b','c')`. Same parse shape as ENUM;
17618            // semantics differ (subset rather than pick-one).
17619            "set" => {
17620                if !matches!(self.peek(), Token::LParen) {
17621                    return Err(self.err(alloc::format!(
17622                        "expected '(' after SET, got {:?}",
17623                        self.peek()
17624                    )));
17625                }
17626                self.advance();
17627                let mut variants: Vec<String> = Vec::new();
17628                loop {
17629                    match self.advance() {
17630                        Token::String(s) => variants.push(s),
17631                        other => {
17632                            return Err(self.err(alloc::format!(
17633                                "SET(...) expects string literal variants, got {other:?}"
17634                            )));
17635                        }
17636                    }
17637                    match self.peek() {
17638                        Token::Comma => {
17639                            self.advance();
17640                            continue;
17641                        }
17642                        Token::RParen => {
17643                            self.advance();
17644                            break;
17645                        }
17646                        other => {
17647                            return Err(self.err(alloc::format!(
17648                                "expected ',' or ')' in SET(...), got {other:?}"
17649                            )));
17650                        }
17651                    }
17652                }
17653                if variants.is_empty() {
17654                    return Err(self.err("SET(...) must declare at least one variant".into()));
17655                }
17656                inline_set_variants = Some(variants);
17657                ColumnTypeName::Text
17658            }
17659            _other => {
17660                // v7.17.0 Phase 1.4 — unknown ident → defer
17661                // resolution to the engine. Stored as Text in
17662                // ColumnTypeName + the original name carried as
17663                // `user_type_ref` so CREATE TABLE can look up
17664                // user-defined enum / domain types.
17665                user_type_ref = Some(ty_ident.clone());
17666                ColumnTypeName::Text
17667            }
17668        };
17669        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
17670        // right after the type keyword. Pre-4.4 SPG consumed +
17671        // discarded the keyword, leaving a customer column
17672        // declared `id INT UNSIGNED NOT NULL` silently accepting
17673        // negative values — a Tier-A correctness drift where
17674        // application invariants (auto-increment-IDs never
17675        // negative) silently broke on cutover. Now: capture as
17676        // a column flag, persist on the schema, enforce at
17677        // INSERT / UPDATE time.
17678        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
17679        {
17680            self.advance();
17681            true
17682        } else {
17683            false
17684        };
17685        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
17686        // `<type> COLLATE <name>` post-fixes on text columns. SPG
17687        // stores text as UTF-8 always so CHARACTER SET is still a
17688        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
17689        // name: it gets classified into a `Collation` variant the
17690        // engine consults at WHERE-eval time. PG `default` /
17691        // `pg_catalog.default` / `C` / `POSIX` collations all
17692        // resolve to `Binary` (the prior behaviour); `_ci` /
17693        // `case_insensitive` / `nocase` shift to CaseInsensitive.
17694        // The schema-qualifier form (`pg_catalog.default`) lexes
17695        // as `Ident '.' Ident` — peek for the `.` and consume both
17696        // halves so it's treated as one collation name. PG's
17697        // `IDENT.IDENT` collation form (which can appear here) is
17698        // resolved by Collation::from_collation_name on the bare
17699        // identifier after the dot.
17700        let mut collation = Collation::Binary;
17701        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
17702        // clause was written. The engine needs this to tell an explicit
17703        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
17704        // clause at all: both resolve to `Collation::Binary`, but under the
17705        // MySQL dialect the latter takes the folding default collation.
17706        let mut collation_explicit = false;
17707        let mut collation_name: Option<alloc::string::String> = None;
17708        loop {
17709            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
17710                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
17711            {
17712                self.advance(); // CHARACTER
17713                self.advance(); // SET
17714                if matches!(
17715                    self.peek(),
17716                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
17717                ) {
17718                    self.advance();
17719                }
17720                continue;
17721            }
17722            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
17723                self.advance(); // COLLATE
17724                // Accept Ident / QuotedIdent / String AND the
17725                // keyword-tokenised `Default` (PG `pg_catalog.default`
17726                // and bare `DEFAULT` collation names — `default` is a
17727                // reserved word so the lexer hands back Token::Default
17728                // not Token::Ident).
17729                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
17730                    match this.peek().clone() {
17731                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
17732                            this.advance();
17733                            Some(s)
17734                        }
17735                        Token::Default => {
17736                            this.advance();
17737                            Some(alloc::string::String::from("default"))
17738                        }
17739                        _ => None,
17740                    }
17741                };
17742                let raw = if let Some(head) = read_collation_atom(self) {
17743                    // Schema-qualified PG form: `pg_catalog.default`.
17744                    if matches!(self.peek(), Token::Dot) {
17745                        self.advance();
17746                        let tail = read_collation_atom(self).unwrap_or_default();
17747                        alloc::format!("{head}.{tail}")
17748                    } else {
17749                        head
17750                    }
17751                } else {
17752                    alloc::string::String::new()
17753                };
17754                if !raw.is_empty() {
17755                    collation_explicit = true;
17756                    // v7.39 (round 676) — keep the name too. The enum below
17757                    // folds C / POSIX / en_US / default into one value, and
17758                    // `pg_attribute.attcollation` has to tell them apart.
17759                    // The schema qualifier goes: PG's `pg_catalog.default`
17760                    // and a bare `default` name the same collation.
17761                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17762                    // encoding suffix. Round 676 used `rsplit('.')` for
17763                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17764                    // PG writes `pg_catalog.default` (qualifier) and
17765                    // `en_US.utf8` (locale + encoding) with the same
17766                    // separator. Only `pg_catalog.` is a qualifier, and it
17767                    // is the only one PG's own dumps emit.
17768                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17769                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17770                    collation_name = Some(alloc::string::String::from(bare));
17771                    let parsed = Collation::from_collation_name(&raw);
17772                    // Last COLLATE clause wins, but `Binary` from a
17773                    // bare keyword like `default` should not
17774                    // silently downgrade a stronger one set earlier
17775                    // on the same column. v7.17 only ships one
17776                    // non-Binary variant so a simple OR is enough.
17777                    if parsed != Collation::Binary {
17778                        collation = parsed;
17779                    }
17780                }
17781                continue;
17782            }
17783            break;
17784        }
17785        // v7.10.10 — postfix `[]` widens the base type to its array
17786        // type. PG accepts `TYPE[]` after any base type and so does
17787        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17788        // all through; the old "only TEXT[]" note was stale).
17789        if matches!(self.peek(), Token::LBracket) {
17790            self.advance();
17791            if !matches!(self.peek(), Token::RBracket) {
17792                return Err(self.err(alloc::format!(
17793                    "TEXT[] takes no dimension; got {:?}",
17794                    self.peek()
17795                )));
17796            }
17797            self.advance();
17798            // v7.11.13 — widened to INT[] and BIGINT[] in addition
17799            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17800            // still error here.
17801            ty = match ty {
17802                ColumnTypeName::Text => ColumnTypeName::TextArray,
17803                ColumnTypeName::Int => ColumnTypeName::IntArray,
17804                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17805                // v7.40.0 — `oid[]`. Everything downstream of the
17806                // parser already handled `DataType::OidArray`; this
17807                // arm is the whole of what was missing.
17808                ColumnTypeName::Oid => ColumnTypeName::OidArray,
17809                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17810                // `[]` grammar. Wire OID 1187.
17811                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17812                // v7.37.5 γ — full PG array-of-scalar family.
17813                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17814                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17815                ColumnTypeName::Float => ColumnTypeName::FloatArray,
17816                // NUMERIC(p, s) loses its precision params at the
17817                // array level (matches PG: `NUMERIC[]` is untyped,
17818                // per-element precision flows through values).
17819                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17820                ColumnTypeName::Date => ColumnTypeName::DateArray,
17821                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17822                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17823                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17824                ColumnTypeName::Json => ColumnTypeName::JsonArray,
17825                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17826                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17827                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17828                // the array level (matches PG semantics where the
17829                // element precision is per-row, not column-wide).
17830                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17831                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17832                // v7.40.0 — TIME(p)[] / TIMETZ(p)[] drop the
17833                // precision the same way NUMERIC[] does.
17834                ColumnTypeName::Real => ColumnTypeName::RealArray,
17835                ColumnTypeName::Time => ColumnTypeName::TimeArray,
17836                ColumnTypeName::TimeTz => ColumnTypeName::TimeTzArray,
17837                ColumnTypeName::Inet => ColumnTypeName::InetArray,
17838                ColumnTypeName::Xml => ColumnTypeName::XmlArray,
17839                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17840                // follow-up.
17841                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17842                other => {
17843                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17844                }
17845            };
17846            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17847            // for INT/TEXT/BIGINT. Anything else is an error.
17848            if matches!(self.peek(), Token::LBracket) {
17849                self.advance();
17850                if !matches!(self.peek(), Token::RBracket) {
17851                    return Err(self.err(alloc::format!(
17852                        "TYPE[][] second dimension takes no size; got {:?}",
17853                        self.peek()
17854                    )));
17855                }
17856                self.advance();
17857                ty = match ty {
17858                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17859                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17860                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17861                    // v7.39 (read01 round 75) — bool[][].
17862                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17863                    other => {
17864                        return Err(self.err(alloc::format!(
17865                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17866                             TEXT[][] only; got {other:?}"
17867                        )));
17868                    }
17869                };
17870            }
17871        }
17872        Ok((
17873            ty,
17874            implied_auto_increment,
17875            implied_not_null,
17876            user_type_ref,
17877            collation,
17878            collation_explicit,
17879            collation_name,
17880            is_unsigned,
17881            inline_enum_variants,
17882            inline_set_variants,
17883            mysql_int_width,
17884            mysql_fsp,
17885            mysql_declared_timestamp,
17886            mysql_float_md,
17887        ))
17888    }
17889
17890    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17891        // v7.20 — PG reserves the table-constraint keywords, so a
17892        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17893        // malformed constraint clause (e.g. `UNIQUE a` missing its
17894        // parens), not a column named "unique". Since v7.17's
17895        // unknown-type leniency (`user_type_ref`) such a clause
17896        // would otherwise parse as a column with a user-defined
17897        // type — silently accepting invalid DDL. Quoted
17898        // identifiers ("unique" / `unique`) remain valid names.
17899        if let Token::Ident(s) = self.peek()
17900            && [
17901                "unique",
17902                "primary",
17903                "foreign",
17904                "constraint",
17905                "check",
17906                "references",
17907                "exclude",
17908            ]
17909            .iter()
17910            .any(|kw| s.eq_ignore_ascii_case(kw))
17911        {
17912            return Err(self.err(alloc::format!(
17913                "unexpected reserved keyword '{s}' at start of column definition \
17914                 (malformed table constraint?)"
17915            )));
17916        }
17917        let name_tok = self.pos;
17918        let name = self.expect_ident_like()?;
17919        // v7.39.3 — MySQL 9.7.2 reports a column by the SPELLING it was
17920        // declared with: `MyCol` stays `MyCol` in SHOW COLUMNS, in
17921        // information_schema, and in SHOW CREATE (measured). SPG folded
17922        // an unquoted name, so a table restored from a dump reported
17923        // names the application had never written.
17924        //
17925        // The written form comes back from the source span, which only
17926        // the MySQL dialect keeps. The span runs to the START of the
17927        // next token, so a comment or unusual spacing between them
17928        // arrives with it — hence the check that what came back is the
17929        // same identifier. It is not decoration: without it,
17930        // `CREATE TABLE t (MyCol /* c */ INT)` names the column
17931        // `MyCol /* c */`.
17932        let name = self
17933            .source_span(name_tok, name_tok)
17934            .map(|raw| raw.trim().trim_matches('`').trim_matches('"'))
17935            .filter(|raw| raw.eq_ignore_ascii_case(&name))
17936            .map_or(name, alloc::string::String::from);
17937        let (
17938            ty,
17939            implied_auto_increment,
17940            implied_not_null,
17941            user_type_ref,
17942            collation,
17943            collation_explicit,
17944            collation_name,
17945            is_unsigned,
17946            inline_enum_variants,
17947            inline_set_variants,
17948            mysql_int_width,
17949            mysql_fsp,
17950            mysql_declared_timestamp,
17951            mysql_float_md,
17952        ) = self.parse_type_with_implied_flags()?;
17953        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17954        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17955        // each at most once.
17956        let mut default: Option<Expr> = None;
17957        let mut nullable = !implied_not_null;
17958        let mut nullability_seen = implied_not_null;
17959        let mut auto_increment = implied_auto_increment;
17960        let mut is_primary_key = false;
17961        let mut is_unique = false;
17962        let mut unique_nulls_not_distinct = false;
17963        let mut constraint_deferrable = false;
17964        let mut constraint_initially_deferred = false;
17965        let mut check: Option<Expr> = None;
17966        let mut on_update_runtime: Option<Expr> = None;
17967        let mut generated_stored_expr: Option<Box<Expr>> = None;
17968        let mut identity_always = false;
17969        loop {
17970            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17971            // not-null constraints by name and pg_dump emits them
17972            // inline: `id bigint CONSTRAINT contacts_id_not_null1
17973            // NOT NULL`. Accept and discard the name; whatever
17974            // constraint follows is parsed by the arms below.
17975            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17976                // v7.39 (round 308, V29) — a name on an inline
17977                // REFERENCES belongs to the FOREIGN KEY, and the caller
17978                // (`parse_column_def_with_fk`) is what builds it, so
17979                // leave the whole clause for it. Dropping the name here
17980                // is what made `CONSTRAINT fk_a REFERENCES …` come back
17981                // as the synthesised `c_pid_fkey` — which then could
17982                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17983                // `advance()` takes tokens by `mem::replace`, so there
17984                // is no rewinding once consumed.
17985                if matches!(
17986                    self.tokens.get(self.pos + 2),
17987                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17988                ) {
17989                    break;
17990                }
17991                self.advance();
17992                let _name = self.expect_ident_like()?;
17993                continue;
17994            }
17995            // v7.39 (round 379) — MySQL's SHORT generated-column form
17996            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17997            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17998            // below), but hand-written schemas and app migrations use this.
17999            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
18000            // SPG computes-and-stores either way, like the long form.
18001            if matches!(self.peek(), Token::As) {
18002                self.advance();
18003                if !matches!(self.peek(), Token::LParen) {
18004                    return Err(self.err(alloc::format!(
18005                        "expected '(' after AS in a generated column, got {:?}",
18006                        self.peek()
18007                    )));
18008                }
18009                self.advance();
18010                let expr = self.parse_expr(0)?;
18011                if !matches!(self.peek(), Token::RParen) {
18012                    return Err(self.err(alloc::format!(
18013                        "expected ')' after AS (<expr>), got {:?}",
18014                        self.peek()
18015                    )));
18016                }
18017                self.advance();
18018                if matches!(self.peek(), Token::Ident(s)
18019                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
18020                {
18021                    self.advance();
18022                }
18023                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
18024                continue;
18025            }
18026            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
18027            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
18028            // the modern replacement for SERIAL in hand-written
18029            // schemas). Both flavours map onto the auto-increment
18030            // machinery — SPG's serial semantics ≈ BY DEFAULT;
18031            // ALWAYS's reject-explicit-values nuance is documented
18032            // leniency. Generated EXPRESSION columns
18033            // (`AS (expr) STORED`) are not supported: error loudly
18034            // instead of silently storing NULLs.
18035            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
18036                self.advance();
18037                let mut saw_generated_always = false;
18038                match self.peek().clone() {
18039                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
18040                        self.advance();
18041                        saw_generated_always = true;
18042                    }
18043                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
18044                        self.advance();
18045                        if !matches!(self.peek(), Token::Default) {
18046                            return Err(self.err(alloc::format!(
18047                                "expected DEFAULT after GENERATED BY, got {:?}",
18048                                self.peek()
18049                            )));
18050                        }
18051                        self.advance();
18052                    }
18053                    other => {
18054                        return Err(self.err(alloc::format!(
18055                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
18056                        )));
18057                    }
18058                }
18059                if !matches!(self.peek(), Token::As) {
18060                    return Err(self.err(alloc::format!(
18061                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
18062                        self.peek()
18063                    )));
18064                }
18065                self.advance();
18066                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
18067                // ( <expr> ) STORED` stored computed-column. The
18068                // expression is captured for the engine to recompute
18069                // on every INSERT / UPDATE. v7.37.7 accepts the
18070                // STORED keyword only; PG also has VIRTUAL, which
18071                // v7.37.7 carves out (sentori only uses STORED).
18072                if matches!(self.peek(), Token::LParen) {
18073                    self.advance();
18074                    let expr = self.parse_expr(0)?;
18075                    if !matches!(self.peek(), Token::RParen) {
18076                        return Err(self.err(alloc::format!(
18077                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
18078                            self.peek()
18079                        )));
18080                    }
18081                    self.advance();
18082                    let stored = match self.peek() {
18083                        Token::Ident(s) | Token::QuotedIdent(s)
18084                            if s.eq_ignore_ascii_case("stored") =>
18085                        {
18086                            self.advance();
18087                            true
18088                        }
18089                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
18090                        // generated columns. SPG computes them on write and
18091                        // persists like STORED; the two are observably
18092                        // identical for query results (the value, recompute
18093                        // on base-column change, and NOT NULL enforcement all
18094                        // match), so a PG 18 schema/dump using VIRTUAL loads
18095                        // and behaves correctly. The compute-on-read storage
18096                        // saving is an invisible internal difference.
18097                        Token::Ident(s) | Token::QuotedIdent(s)
18098                            if s.eq_ignore_ascii_case("virtual") =>
18099                        {
18100                            self.advance();
18101                            false
18102                        }
18103                        other => {
18104                            return Err(self.err(alloc::format!(
18105                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
18106                                 got {other:?}"
18107                            )));
18108                        }
18109                    };
18110                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
18111                    generated_stored_expr = Some(Box::new(expr));
18112                    continue;
18113                }
18114                self.expect_keyword_ident("identity")?;
18115                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
18116                // consume the balanced parens and discard (SPG's
18117                // auto-increment is max+1-scan based).
18118                if matches!(self.peek(), Token::LParen) {
18119                    let mut depth = 0usize;
18120                    loop {
18121                        match self.advance() {
18122                            Token::LParen => depth += 1,
18123                            Token::RParen => {
18124                                depth -= 1;
18125                                if depth == 0 {
18126                                    break;
18127                                }
18128                            }
18129                            Token::Eof => {
18130                                return Err(self.err(
18131                                    "unterminated sequence-options parens after IDENTITY".into(),
18132                                ));
18133                            }
18134                            _ => {}
18135                        }
18136                    }
18137                }
18138                auto_increment = true;
18139                // v7.38 (read01) — remember the ALWAYS flavour so the engine
18140                // can reject explicit non-DEFAULT INSERT values (unless
18141                // OVERRIDING SYSTEM VALUE) the way PG does.
18142                identity_always = saw_generated_always;
18143                // PG identity columns are implicitly NOT NULL.
18144                nullable = false;
18145                continue;
18146            }
18147            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
18148            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
18149            // is accepted today. The "ON" token is an Ident
18150            // (not reserved) — peek before consuming.
18151            if matches!(self.peek(), Token::On)
18152                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
18153            {
18154                self.advance(); // ON
18155                self.advance(); // update
18156                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
18157                let next = self.peek().clone();
18158                match next {
18159                    Token::Ident(s) | Token::QuotedIdent(s)
18160                        if s.eq_ignore_ascii_case("current_timestamp") =>
18161                    {
18162                        self.advance();
18163                        // Optional `(N)` precision.
18164                        if matches!(self.peek(), Token::LParen) {
18165                            self.advance();
18166                            if !matches!(self.peek(), Token::Integer(_)) {
18167                                return Err(self.err(alloc::format!(
18168                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
18169                                    self.peek()
18170                                )));
18171                            }
18172                            self.advance();
18173                            if !matches!(self.peek(), Token::RParen) {
18174                                return Err(self.err(alloc::format!(
18175                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
18176                                    self.peek()
18177                                )));
18178                            }
18179                            self.advance();
18180                        }
18181                        on_update_runtime = Some(Expr::FunctionCall {
18182                            name: "now".into(),
18183                            args: Vec::new(),
18184                        });
18185                        continue;
18186                    }
18187                    other => {
18188                        return Err(self.err(alloc::format!(
18189                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
18190                        )));
18191                    }
18192                }
18193            }
18194            if matches!(self.peek(), Token::Default) {
18195                if default.is_some() {
18196                    return Err(self.err("DEFAULT specified twice".into()));
18197                }
18198                self.advance();
18199                default = Some(self.parse_expr(0)?);
18200                continue;
18201            }
18202            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
18203            // token with NOT NULL and sits EARLIER in the loop than the
18204            // deferrability arm, so without the lookahead it was reported as
18205            // "NOT NULL specified twice" (or "expected NULL after NOT").
18206            if matches!(self.peek(), Token::Not)
18207                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
18208            {
18209                // NOT DEFERRABLE — explicit immediate; nothing to carry.
18210                self.consume_optional_deferrable_clauses()?;
18211                continue;
18212            }
18213            if matches!(self.peek(), Token::Not) {
18214                if nullability_seen {
18215                    return Err(self.err("NOT NULL specified twice".into()));
18216                }
18217                self.advance();
18218                if !matches!(self.peek(), Token::Null) {
18219                    return Err(self.err(format!(
18220                        "expected NULL after NOT in column def, got {:?}",
18221                        self.peek()
18222                    )));
18223                }
18224                self.advance();
18225                nullable = false;
18226                nullability_seen = true;
18227                continue;
18228            }
18229            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
18230            // "this column is nullable" marker (the default in
18231            // standard SQL anyway). mysqldump emits it routinely
18232            // (`col TYPE NULL DEFAULT NULL` for nullable
18233            // timestamps etc). Accept + no-op.
18234            if matches!(self.peek(), Token::Null) {
18235                if nullability_seen && !nullable {
18236                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
18237                    // sentence, PG18-measured (the table name is the
18238                    // caller's; the column half is exact).
18239                    return Err(self.err(alloc::format!(
18240                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
18241                    )));
18242                }
18243                self.advance();
18244                nullable = true;
18245                nullability_seen = true;
18246                continue;
18247            }
18248            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
18249            // arrives as a bare Ident. Match either, case-insensitive.
18250            if let Token::Ident(s) = self.peek()
18251                && (s.eq_ignore_ascii_case("auto_increment")
18252                    || s.eq_ignore_ascii_case("autoincrement"))
18253            {
18254                if auto_increment {
18255                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
18256                }
18257                self.advance();
18258                auto_increment = true;
18259                continue;
18260            }
18261            // v7.9.13 — inline `PRIMARY KEY` column constraint
18262            // (mailrs F1). Implies `NOT NULL`. The engine creates
18263            // a BTree index for the PK column at CREATE TABLE time
18264            // so FK parent-side index lookups resolve.
18265            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
18266            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
18267            // spelling was a parse error, so a pg_dump carrying one stopped
18268            // mid-restore. The clauses are consumed by the same helper the FK
18269            // path has used since round 288 and recorded nowhere: SPG enforces
18270            // the constraint IMMEDIATELY either way, which fails earlier than
18271            // PG inside a transaction that violates-then-repairs — a refusal,
18272            // not a wrong answer. True deferral is the open remainder of F08.
18273            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
18274                || (matches!(self.peek(), Token::Not)
18275                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
18276            {
18277                // v7.39 (round 711) — CARRIED now (the storing half of
18278                // F08); round 621 only consumed.
18279                let (d, idef) = self.consume_deferrable_clauses_timed()?;
18280                constraint_deferrable |= d;
18281                constraint_initially_deferred |= idef;
18282                continue;
18283            }
18284            if let Token::Ident(s) = self.peek()
18285                && s.eq_ignore_ascii_case("primary")
18286            {
18287                if is_primary_key {
18288                    return Err(self.err("PRIMARY KEY specified twice".into()));
18289                }
18290                // Peek-ahead for the required `KEY` token.
18291                let next = self.tokens.get(self.pos + 1);
18292                let next_is_key = matches!(
18293                    next,
18294                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
18295                );
18296                if !next_is_key {
18297                    return Err(self.err(format!(
18298                        "expected KEY after PRIMARY in column def, got {:?}",
18299                        next
18300                    )));
18301                }
18302                self.advance(); // PRIMARY
18303                self.advance(); // KEY
18304                is_primary_key = true;
18305                if nullability_seen && nullable {
18306                    return Err(self.err(
18307                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
18308                    ));
18309                }
18310                nullable = false;
18311                nullability_seen = true;
18312                continue;
18313            }
18314            // v7.13.0 — inline `UNIQUE` column constraint
18315            // (mailrs round-5 G2). Fold into a single-column
18316            // table-level UNIQUE at CREATE TABLE post-process time.
18317            if let Token::Ident(s) = self.peek()
18318                && s.eq_ignore_ascii_case("unique")
18319            {
18320                if is_unique {
18321                    return Err(self.err("UNIQUE specified twice".into()));
18322                }
18323                self.advance();
18324                is_unique = true;
18325                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
18326                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
18327                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
18328                    let n1 = self.tokens.get(self.pos + 1);
18329                    let n2 = self.tokens.get(self.pos + 2);
18330                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
18331                        self.advance(); // NULLS
18332                        self.advance(); // NOT
18333                        self.advance(); // DISTINCT
18334                        unique_nulls_not_distinct = true;
18335                    } else if matches!(n1, Some(Token::Distinct)) {
18336                        self.advance(); // NULLS
18337                        self.advance(); // DISTINCT
18338                    }
18339                }
18340                continue;
18341            }
18342            // v7.13.0 — inline `CHECK (<expr>)` column constraint
18343            // (mailrs round-5 G3). PG semantics: column-level
18344            // CHECK is equivalent to a table-level CHECK. Multiple
18345            // inline CHECKs on the same column AND together.
18346            if let Token::Ident(s) = self.peek()
18347                && s.eq_ignore_ascii_case("check")
18348            {
18349                self.advance();
18350                if !matches!(self.peek(), Token::LParen) {
18351                    return Err(self.err(alloc::format!(
18352                        "expected '(' after CHECK in column def, got {:?}",
18353                        self.peek()
18354                    )));
18355                }
18356                self.advance();
18357                let pred = self.parse_expr(0)?;
18358                if !matches!(self.peek(), Token::RParen) {
18359                    return Err(self.err(alloc::format!(
18360                        "expected ')' to close CHECK predicate, got {:?}",
18361                        self.peek()
18362                    )));
18363                }
18364                self.advance();
18365                check = Some(match check.take() {
18366                    Some(prev) => Expr::Binary {
18367                        op: BinOp::And,
18368                        lhs: Box::new(prev),
18369                        rhs: Box::new(pred),
18370                    },
18371                    None => pred,
18372                });
18373                continue;
18374            }
18375            break;
18376        }
18377        Ok(ColumnDef {
18378            name,
18379            ty,
18380            nullable,
18381            default,
18382            auto_increment,
18383            is_primary_key,
18384            is_unique,
18385            unique_nulls_not_distinct,
18386            constraint_deferrable,
18387            constraint_initially_deferred,
18388            check,
18389            user_type_ref,
18390            on_update_runtime,
18391            collation,
18392            collation_explicit,
18393            collation_name,
18394            is_unsigned,
18395            inline_enum_variants,
18396            inline_set_variants,
18397            generated_stored_expr,
18398            identity_always,
18399            mysql_int_width,
18400            mysql_fsp,
18401            mysql_declared_timestamp,
18402            mysql_float_md,
18403        })
18404    }
18405
18406    /// `NUMERIC` may appear without parameters, with one (precision
18407    /// only, scale=0), or with both. Returns `(precision, scale)` with
18408    /// 0 = unspecified for the bare form.
18409    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
18410        if !matches!(self.peek(), Token::LParen) {
18411            // Bare `NUMERIC` — PG treats this as "unlimited precision";
18412            // we surface it as precision=0 to mean "unconstrained" so
18413            // the engine doesn't need a separate variant.
18414            return Ok((0, 0));
18415        }
18416        self.advance();
18417        // v7.39 (round 272) — PG's declared precision runs to 1000, and
18418        // it words the out-of-range case with the value it saw. SPG
18419        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
18420        // accepts failed to parse at all; values wider than i128 are
18421        // carried by the arbitrary-precision form.
18422        let precision = match self.advance() {
18423            Token::Integer(n) if (1..=1000).contains(&n) => {
18424                u16::try_from(n).expect("range-checked")
18425            }
18426            Token::Integer(n) => {
18427                return Err(ParseError {
18428                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
18429                    token_pos: self.consumed_pos(),
18430                });
18431            }
18432            other => {
18433                return Err(ParseError {
18434                    message: format!(
18435                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
18436                    ),
18437                    token_pos: self.consumed_pos(),
18438                });
18439            }
18440        };
18441        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
18442        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
18443        // then overflows). A negative scale rounds to tens / hundreds / …
18444        let scale = if matches!(self.peek(), Token::Comma) {
18445            self.advance();
18446            let neg = if matches!(self.peek(), Token::Minus) {
18447                self.advance();
18448                true
18449            } else {
18450                false
18451            };
18452            match self.advance() {
18453                Token::Integer(n) => {
18454                    let signed = if neg { -n } else { n };
18455                    if !(-1000..=1000).contains(&signed) {
18456                        return Err(ParseError {
18457                            message: format!(
18458                                "NUMERIC scale {signed} must be between -1000 and 1000"
18459                            ),
18460                            token_pos: self.consumed_pos(),
18461                        });
18462                    }
18463                    i16::try_from(signed).expect("range-checked")
18464                }
18465                other => {
18466                    return Err(ParseError {
18467                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
18468                        token_pos: self.consumed_pos(),
18469                    });
18470                }
18471            }
18472        } else {
18473            0
18474        };
18475        if !matches!(self.peek(), Token::RParen) {
18476            return Err(self.err(format!(
18477                "expected ')' to close NUMERIC params, got {:?}",
18478                self.peek()
18479            )));
18480        }
18481        self.advance();
18482        Ok((precision, scale))
18483    }
18484
18485    /// Parse `(N)` where `N` is a positive integer literal — used by the
18486    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
18487    /// for the error message.
18488    /// v6.0.1: parse the optional `USING <encoding>` clause that
18489    /// follows `VECTOR(N)` in a column definition. Missing clause
18490    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
18491    /// ident → `ParseError` listing the encodings recognised today.
18492    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
18493        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
18494            return Ok(VecEncoding::F32);
18495        }
18496        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
18497        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
18498        // consume the token when the very next token is a known
18499        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
18500        // `USING` for the caller — it's the rewrite-expression form.
18501        let n1 = self.tokens.get(self.pos + 1);
18502        let next_is_encoding = matches!(
18503            n1,
18504            Some(Token::Ident(s))
18505                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
18506        );
18507        if !next_is_encoding {
18508            return Ok(VecEncoding::F32);
18509        }
18510        self.advance();
18511        let enc_ident = match self.advance() {
18512            Token::Ident(s) => s,
18513            other => {
18514                return Err(self.err(format!(
18515                    "expected vector encoding after USING, got {other:?}"
18516                )));
18517            }
18518        };
18519        match enc_ident.to_ascii_lowercase().as_str() {
18520            "sq8" => Ok(VecEncoding::Sq8),
18521            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
18522            // binary16 per-element storage.
18523            "half" => Ok(VecEncoding::F16),
18524            other => Err(self.err(format!(
18525                "unknown vector encoding {other:?}; supported: SQ8, HALF"
18526            ))),
18527        }
18528    }
18529
18530    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
18531    /// without consuming it. Returns `Some(N)` when the next
18532    /// tokens are `( <int> )`; None otherwise. Used by the
18533    /// TINYINT classifier to decide whether to map to Bool or
18534    /// SmallInt.
18535    fn peek_optional_paren_size_value(&self) -> Option<i64> {
18536        if !matches!(self.peek(), Token::LParen) {
18537            return None;
18538        }
18539        let next = self.tokens.get(self.pos + 1)?;
18540        let n = match next {
18541            Token::Integer(n) => *n,
18542            _ => return None,
18543        };
18544        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18545            return None;
18546        }
18547        Some(n)
18548    }
18549
18550    /// v7.14.0 — consume an optional MySQL display-width
18551    /// parenthesised number after an integer type, returning
18552    /// nothing. `TINYINT(1)` etc.
18553    /// v7.39 (round 360) — does the parenthesised group ahead contain a
18554    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
18555    fn peek_paren_has_comma(&self) -> bool {
18556        let mut i = self.pos + 1;
18557        let mut depth = 1usize;
18558        while depth > 0 {
18559            match self.tokens.get(i) {
18560                Some(Token::LParen) => depth += 1,
18561                Some(Token::RParen) => depth -= 1,
18562                Some(Token::Comma) if depth == 1 => return true,
18563                None | Some(Token::Eof) => return false,
18564                _ => {}
18565            }
18566            i += 1;
18567        }
18568        false
18569    }
18570
18571    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
18572    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
18573    /// fractional-seconds precision that drives write truncation and render
18574    /// padding, where `consume_optional_paren_size` throws it away.
18575    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
18576    fn take_optional_paren_size(&mut self) -> Option<u8> {
18577        let Some(Token::Integer(n)) = self
18578            .tokens
18579            .get(self.pos + 1)
18580            .filter(|_| matches!(self.peek(), Token::LParen))
18581            .cloned()
18582        else {
18583            self.consume_optional_paren_size();
18584            return None;
18585        };
18586        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18587            self.consume_optional_paren_size();
18588            return None;
18589        }
18590        self.consume_optional_paren_size();
18591        u8::try_from(n).ok()
18592    }
18593
18594    fn consume_optional_paren_size(&mut self) {
18595        if !matches!(self.peek(), Token::LParen) {
18596            return;
18597        }
18598        self.advance();
18599        // Skip until matching RParen (allow nested or any tokens).
18600        let mut depth = 1usize;
18601        while depth > 0 {
18602            match self.peek() {
18603                Token::LParen => depth += 1,
18604                Token::RParen => depth -= 1,
18605                Token::Eof => return,
18606                _ => {}
18607            }
18608            self.advance();
18609        }
18610    }
18611
18612    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
18613        if !matches!(self.peek(), Token::LParen) {
18614            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
18615        }
18616        self.advance();
18617        let n = match self.advance() {
18618            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
18619                message: format!("{label} size too large: {n}"),
18620                token_pos: self.consumed_pos(),
18621            })?,
18622            other => {
18623                return Err(ParseError {
18624                    message: format!("expected positive integer {label} size, got {other:?}"),
18625                    token_pos: self.consumed_pos(),
18626                });
18627            }
18628        };
18629        if !matches!(self.peek(), Token::RParen) {
18630            return Err(self.err(format!(
18631                "expected ')' after {label} size, got {:?}",
18632                self.peek()
18633            )));
18634        }
18635        self.advance();
18636        Ok(n)
18637    }
18638
18639    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
18640    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
18641    /// key, like MySQL) whose action skips conflicting rows.
18642    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
18643    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
18644    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
18645    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
18646    /// common bulk-upsert spellings —
18647    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
18648    ///     REPLACE INTO t SELECT …
18649    /// — were a parse error / a duplicate-key failure respectively.
18650    ///
18651    /// Precedence: an explicitly written clause beats a statement-level flag.
18652    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
18653    /// implicit `REPLACE` and `IGNORE` lowerings.
18654    fn parse_insert_conflict_clause(
18655        &mut self,
18656        replace: bool,
18657        ignore: bool,
18658    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18659        if let Some(c) = self.parse_optional_on_duplicate_key()? {
18660            return Ok(Some(c));
18661        }
18662        if let Some(c) = self.parse_optional_on_conflict()? {
18663            return Ok(Some(c));
18664        }
18665        if replace {
18666            // REPLACE INTO = delete-then-insert, which PG spells as
18667            // `ON CONFLICT DO UPDATE SET` over every column; the engine
18668            // reads an empty assignment list as "take the incoming row".
18669            return Ok(Some(crate::ast::OnConflictClause {
18670                target_columns: Vec::new(),
18671                index_where: None,
18672                constraint_name: None,
18673                mysql_lowered: true,
18674                action: crate::ast::OnConflictAction::Update {
18675                    assignments: Vec::new(),
18676                    where_: None,
18677                },
18678            }));
18679        }
18680        if ignore {
18681            return Ok(Some(Self::insert_ignore_clause()));
18682        }
18683        Ok(None)
18684    }
18685
18686    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
18687    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
18688    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
18689    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
18690    fn parse_optional_on_duplicate_key(
18691        &mut self,
18692    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18693        if !(matches!(self.peek(), Token::On)
18694            && matches!(self.tokens.get(self.pos + 1),
18695                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
18696        {
18697            return Ok(None);
18698        }
18699        self.advance(); // ON
18700        self.advance(); // DUPLICATE
18701        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
18702            return Err(self.err(format!(
18703                "expected KEY after ON DUPLICATE, got {:?}",
18704                self.peek()
18705            )));
18706        }
18707        self.advance();
18708        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
18709            return Err(self.err(format!(
18710                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
18711                self.peek()
18712            )));
18713        }
18714        self.advance();
18715        let mut assignments: Vec<(String, Expr)> = Vec::new();
18716        loop {
18717            let col = self.expect_ident_like()?;
18718            if !matches!(self.peek(), Token::Eq) {
18719                return Err(self.err(format!(
18720                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
18721                    self.peek()
18722                )));
18723            }
18724            self.advance();
18725            let mut expr = self.parse_expr(0)?;
18726            Self::rewrite_mysql_values_refs(&mut expr);
18727            assignments.push((col, expr));
18728            if matches!(self.peek(), Token::Comma) {
18729                self.advance();
18730                continue;
18731            }
18732            break;
18733        }
18734        Ok(Some(crate::ast::OnConflictClause {
18735            target_columns: Vec::new(),
18736            index_where: None,
18737            constraint_name: None,
18738            mysql_lowered: true,
18739            action: crate::ast::OnConflictAction::Update {
18740                assignments,
18741                where_: None,
18742            },
18743        }))
18744    }
18745
18746    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
18747        crate::ast::OnConflictClause {
18748            target_columns: Vec::new(),
18749            index_where: None,
18750            constraint_name: None,
18751            mysql_lowered: true,
18752            action: crate::ast::OnConflictAction::Nothing,
18753        }
18754    }
18755
18756    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
18757        debug_assert!(
18758            matches!(self.peek(), Token::Insert)
18759                || (replace
18760                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
18761        );
18762        self.advance();
18763        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
18764        // would raise a duplicate-key error instead of failing the statement,
18765        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
18766        // plain ident to the lexer; only the MySQL dialect accepts it here.
18767        let ignore = self.mysql_dialect
18768            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
18769        if ignore {
18770            self.advance();
18771        }
18772        if !matches!(self.peek(), Token::Into) {
18773            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
18774        }
18775        self.advance();
18776        let table = self.expect_ident_like()?;
18777        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
18778        // grammar requires the AS keyword here (a bare identifier would be
18779        // ambiguous with a column list). The alias is what the ON CONFLICT
18780        // DO UPDATE expressions refer to the target row by.
18781        let alias = if matches!(self.peek(), Token::As) {
18782            self.advance();
18783            Some(self.expect_ident_like()?)
18784        } else {
18785            None
18786        };
18787        // v7.39 (round 428) — MySQL's SET-form INSERT:
18788        //     INSERT INTO t SET a = 1, b = 'x'
18789        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18790        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18791        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18792        // measured). So it lowers to the column list + one VALUES row and
18793        // rejoins the ordinary path, which already handles every one of
18794        // those. PG has no such spelling, hence the dialect gate.
18795        if self.mysql_dialect
18796            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18797        {
18798            self.advance(); // SET
18799            let mut names = Vec::new();
18800            let mut values = Vec::new();
18801            loop {
18802                names.push(self.expect_ident_like()?);
18803                if !matches!(self.peek(), Token::Eq) {
18804                    return Err(self.err(alloc::format!(
18805                        "expected '=' in INSERT … SET, got {:?}",
18806                        self.peek()
18807                    )));
18808                }
18809                self.advance();
18810                // `SET a = DEFAULT` rides the same `__column_default` marker
18811                // the VALUES-row and UPDATE-SET paths use; the INSERT
18812                // executor resolves it against the target column.
18813                if matches!(self.peek(), Token::Default) {
18814                    self.advance();
18815                    values.push(Expr::FunctionCall {
18816                        name: "__column_default".to_string(),
18817                        args: Vec::new(),
18818                    });
18819                } else {
18820                    values.push(self.parse_expr(0)?);
18821                }
18822                if matches!(self.peek(), Token::Comma) {
18823                    self.advance();
18824                    continue;
18825                }
18826                break;
18827            }
18828            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18829            let returning = self.parse_optional_returning()?;
18830            return Ok(Statement::Insert(InsertStatement {
18831                ctes: Vec::new(),
18832                table,
18833                alias,
18834                columns: Some(names),
18835                rows: alloc::vec![values],
18836                select_source: None,
18837                // MySQL's SET form has no `OVERRIDING …` clause (that is
18838                // PG's identity-column spelling).
18839                overriding: Overriding::None,
18840                mysql_ignore: ignore,
18841                on_conflict,
18842                returning,
18843            }));
18844        }
18845        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18846        // v7.39 (round 151) — a SELECT or WITH right after the paren is
18847        // a parenthesized query source instead (PG select_with_parens:
18848        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18849        // both keywords are reserved in PG, so no column list can start
18850        // with them.
18851        let columns = if matches!(self.peek(), Token::LParen) {
18852            self.advance();
18853            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18854                let select_stmt = if self.peek_is_with_kw() {
18855                    self.advance();
18856                    self.parse_nested_with_select()?
18857                } else {
18858                    match self.parse_select_stmt()? {
18859                        Statement::Select(s) => s,
18860                        other => {
18861                            return Err(self.err(alloc::format!(
18862                                "expected SELECT in parenthesized INSERT source, got {other:?}"
18863                            )));
18864                        }
18865                    }
18866                };
18867                if !matches!(self.peek(), Token::RParen) {
18868                    return Err(self.err(format!(
18869                        "expected ')' after parenthesized INSERT source, got {:?}",
18870                        self.peek()
18871                    )));
18872                }
18873                self.advance();
18874                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18875                let returning = self.parse_optional_returning()?;
18876                return Ok(Statement::Insert(InsertStatement {
18877                    ctes: Vec::new(),
18878                    table,
18879                    alias: alias.clone(),
18880                    columns: None,
18881                    rows: Vec::new(),
18882                    select_source: Some(Box::new(select_stmt)),
18883                    on_conflict,
18884                    returning,
18885                    overriding: Overriding::None,
18886                    mysql_ignore: ignore,
18887                }));
18888            }
18889            let mut names = Vec::new();
18890            loop {
18891                names.push(self.expect_ident_like()?);
18892                match self.peek() {
18893                    Token::Comma => {
18894                        self.advance();
18895                    }
18896                    Token::RParen => {
18897                        self.advance();
18898                        break;
18899                    }
18900                    other => {
18901                        return Err(self.err(format!(
18902                            "expected ',' or ')' in INSERT column list, got {other:?}"
18903                        )));
18904                    }
18905                }
18906            }
18907            Some(names)
18908        } else {
18909            None
18910        };
18911        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18912        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18913        // is captured on the statement so the engine can apply PG's
18914        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18915        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18916        {
18917            self.advance();
18918            let which = self.expect_ident_like()?;
18919            let ov = if which.eq_ignore_ascii_case("system") {
18920                Overriding::System
18921            } else if which.eq_ignore_ascii_case("user") {
18922                Overriding::User
18923            } else {
18924                return Err(self.err(format!(
18925                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18926                )));
18927            };
18928            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18929                return Err(self.err(format!(
18930                    "expected VALUE after OVERRIDING {}, got {:?}",
18931                    which.to_ascii_uppercase(),
18932                    self.peek()
18933                )));
18934            }
18935            self.advance();
18936            ov
18937        } else {
18938            Overriding::None
18939        };
18940        // `INSERT INTO t DEFAULT VALUES` — a single row made
18941        // entirely of column defaults. Lower to the permuted
18942        // column-list path with an empty list: every schema column
18943        // is unmapped, so the engine fills each from its default
18944        // (serials advance, plain defaults evaluate, the rest NULL).
18945        if matches!(self.peek(), Token::Default) {
18946            self.advance();
18947            if !matches!(self.peek(), Token::Values) {
18948                return Err(self.err(format!(
18949                    "expected VALUES after DEFAULT in INSERT, got {:?}",
18950                    self.peek()
18951                )));
18952            }
18953            self.advance();
18954            if columns.is_some() {
18955                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18956            }
18957            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18958            let returning = self.parse_optional_returning()?;
18959            return Ok(Statement::Insert(InsertStatement {
18960                ctes: Vec::new(),
18961                table,
18962                alias: alias.clone(),
18963                columns: Some(Vec::new()),
18964                rows: alloc::vec![Vec::new()],
18965                select_source: None,
18966                on_conflict,
18967                returning,
18968                overriding,
18969                mysql_ignore: ignore,
18970            }));
18971        }
18972        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18973        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18974        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18975        // SELECT …`) heads the SOURCE select, as in PG (the statement's
18976        // own WITH comes before INSERT).
18977        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18978            let select_stmt = if self.peek_is_with_kw() {
18979                self.advance();
18980                self.parse_nested_with_select()?
18981            } else {
18982                match self.parse_select_stmt()? {
18983                    Statement::Select(s) => s,
18984                    other => {
18985                        return Err(self.err(alloc::format!(
18986                            "expected SELECT after INSERT INTO ... target, got {other:?}"
18987                        )));
18988                    }
18989                }
18990            };
18991            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18992            let returning = self.parse_optional_returning()?;
18993            return Ok(Statement::Insert(InsertStatement {
18994                ctes: Vec::new(),
18995                table,
18996                alias: alias.clone(),
18997                columns,
18998                rows: Vec::new(),
18999                select_source: Some(Box::new(select_stmt)),
19000                on_conflict,
19001                returning,
19002                overriding,
19003                mysql_ignore: ignore,
19004            }));
19005        }
19006        if !matches!(self.peek(), Token::Values) {
19007            return Err(self.err(format!(
19008                "expected VALUES or SELECT after table name, got {:?}",
19009                self.peek()
19010            )));
19011        }
19012        self.advance();
19013        if !matches!(self.peek(), Token::LParen) {
19014            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
19015        }
19016        let mut rows = Vec::new();
19017        loop {
19018            // Each iteration consumes one `(expr, expr, …)` tuple.
19019            if !matches!(self.peek(), Token::LParen) {
19020                return Err(self.err(format!(
19021                    "expected '(' for next VALUES tuple, got {:?}",
19022                    self.peek()
19023                )));
19024            }
19025            self.advance();
19026            let mut tuple = Vec::new();
19027            loop {
19028                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
19029                // the column's declared default for that slot. Rides out as the
19030                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
19031                // path uses; the INSERT executor resolves it per target column.
19032                if matches!(self.peek(), Token::Default) {
19033                    self.advance();
19034                    tuple.push(Expr::FunctionCall {
19035                        name: "__column_default".to_string(),
19036                        args: Vec::new(),
19037                    });
19038                } else {
19039                    tuple.push(self.parse_expr(0)?);
19040                }
19041                match self.peek() {
19042                    Token::Comma => {
19043                        self.advance();
19044                    }
19045                    Token::RParen => {
19046                        self.advance();
19047                        break;
19048                    }
19049                    other => {
19050                        return Err(self.err(format!(
19051                            "expected ',' or ')' in VALUES tuple, got {other:?}"
19052                        )));
19053                    }
19054                }
19055            }
19056            if tuple.is_empty() {
19057                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
19058            }
19059            rows.push(tuple);
19060            // Continue with comma-separated tuples.
19061            if matches!(self.peek(), Token::Comma) {
19062                self.advance();
19063            } else {
19064                break;
19065            }
19066        }
19067        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
19068        // to ON CONFLICT DO UPDATE with an empty conflict target
19069        // (the engine picks the table's first unique index, which
19070        // matches MySQL's any-unique-key behaviour for the common
19071        // single-key case). `VALUES(col)` in the assignments is
19072        // MySQL's spelling of EXCLUDED.col.
19073        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
19074        let returning = self.parse_optional_returning()?;
19075        Ok(Statement::Insert(InsertStatement {
19076            ctes: Vec::new(),
19077            table,
19078            alias,
19079            columns,
19080            rows,
19081            select_source: None,
19082            on_conflict,
19083            returning,
19084            overriding,
19085            mysql_ignore: ignore,
19086        }))
19087    }
19088
19089    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
19090    /// the incoming row's value — exactly PG's EXCLUDED.col.
19091    fn rewrite_mysql_values_refs(e: &mut Expr) {
19092        match e {
19093            Expr::FunctionCall { name, args }
19094                if name.eq_ignore_ascii_case("values")
19095                    && args.len() == 1
19096                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
19097            {
19098                let Expr::Column(c) = &args[0] else {
19099                    unreachable!("guarded above");
19100                };
19101                *e = Expr::Column(crate::ast::ColumnName {
19102                    qualifier: Some("EXCLUDED".to_string()),
19103                    name: c.name.clone(),
19104                });
19105            }
19106            Expr::FunctionCall { args, .. } => {
19107                for a in args {
19108                    Self::rewrite_mysql_values_refs(a);
19109                }
19110            }
19111            Expr::Binary { lhs, rhs, .. } => {
19112                Self::rewrite_mysql_values_refs(lhs);
19113                Self::rewrite_mysql_values_refs(rhs);
19114            }
19115            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19116                Self::rewrite_mysql_values_refs(expr);
19117            }
19118            Expr::Case {
19119                operand,
19120                branches,
19121                else_branch,
19122            } => {
19123                if let Some(op) = operand {
19124                    Self::rewrite_mysql_values_refs(op);
19125                }
19126                for (w, t) in branches {
19127                    Self::rewrite_mysql_values_refs(w);
19128                    Self::rewrite_mysql_values_refs(t);
19129                }
19130                if let Some(el) = else_branch {
19131                    Self::rewrite_mysql_values_refs(el);
19132                }
19133            }
19134            _ => {}
19135        }
19136    }
19137
19138    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
19139    /// clause sitting between the INSERT body and the trailing
19140    /// RETURNING. All keywords come in as bare idents; `ON` is
19141    /// a reserved Token though.
19142    fn parse_optional_on_conflict(
19143        &mut self,
19144    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
19145        if !matches!(self.peek(), Token::On) {
19146            return Ok(None);
19147        }
19148        // Peek further: we want exactly "ON CONFLICT ...". If the
19149        // next ident isn't "conflict", let some other parser handle.
19150        let next_is_conflict = matches!(
19151            self.tokens.get(self.pos + 1),
19152            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
19153        );
19154        if !next_is_conflict {
19155            return Ok(None);
19156        }
19157        self.advance(); // ON
19158        self.advance(); // CONFLICT
19159        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
19160        // the constraint instead of listing columns (the pg_dump
19161        // form); the engine resolves it.
19162        let mut constraint_name: Option<String> = None;
19163        if matches!(self.peek(), Token::On) {
19164            self.advance(); // ON
19165            match self.advance() {
19166                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
19167                }
19168                other => {
19169                    return Err(self.err(alloc::format!(
19170                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
19171                    )));
19172                }
19173            }
19174            constraint_name = Some(self.expect_ident_like()?);
19175        }
19176        // Optional `(col [, col]*)` target list.
19177        let mut target_columns: Vec<String> = Vec::new();
19178        if matches!(self.peek(), Token::LParen) {
19179            self.advance();
19180            loop {
19181                target_columns.push(self.expect_ident_like()?);
19182                match self.peek() {
19183                    Token::Comma => {
19184                        self.advance();
19185                    }
19186                    Token::RParen => {
19187                        self.advance();
19188                        break;
19189                    }
19190                    other => {
19191                        return Err(self.err(alloc::format!(
19192                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
19193                        )));
19194                    }
19195                }
19196            }
19197        }
19198        // v7.39 (round 240) — optional index predicate after the target
19199        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
19200        // PARTIAL unique index; SPG's arbiters are full indexes, which
19201        // satisfy any predicate, so it is parsed and carried but not
19202        // consulted (recorded residual: partial-unique-index arbiters).
19203        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
19204            self.advance();
19205            Some(self.parse_expr(0)?)
19206        } else {
19207            None
19208        };
19209        // Required `DO`.
19210        match self.advance() {
19211            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
19212            other => {
19213                return Err(self.err(alloc::format!(
19214                    "expected DO after ON CONFLICT [(…)], got {other:?}"
19215                )));
19216            }
19217        }
19218        // Action: NOTHING | UPDATE SET …
19219        let action = match self.advance() {
19220            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
19221                crate::ast::OnConflictAction::Nothing
19222            }
19223            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
19224                self.parse_on_conflict_update_action()?
19225            }
19226            other => {
19227                return Err(self.err(alloc::format!(
19228                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
19229                )));
19230            }
19231        };
19232        Ok(Some(crate::ast::OnConflictClause {
19233            target_columns,
19234            index_where,
19235            constraint_name,
19236            mysql_lowered: false,
19237            action,
19238        }))
19239    }
19240
19241    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
19242    /// `SET col = expr [, …] [WHERE cond]`. Caller already
19243    /// consumed `UPDATE`.
19244    fn parse_on_conflict_update_action(
19245        &mut self,
19246    ) -> Result<crate::ast::OnConflictAction, ParseError> {
19247        // `SET`
19248        match self.advance() {
19249            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
19250            other => {
19251                return Err(self.err(alloc::format!(
19252                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
19253                )));
19254            }
19255        }
19256        let mut assignments: Vec<(String, Expr)> = Vec::new();
19257        loop {
19258            let col = self.expect_ident_like()?;
19259            if !matches!(self.peek(), Token::Eq) {
19260                return Err(self.err(alloc::format!(
19261                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
19262                    self.peek()
19263                )));
19264            }
19265            self.advance();
19266            let value = self.parse_expr(0)?;
19267            assignments.push((col, value));
19268            if matches!(self.peek(), Token::Comma) {
19269                self.advance();
19270                continue;
19271            }
19272            break;
19273        }
19274        let where_ = if matches!(self.peek(), Token::Where) {
19275            self.advance();
19276            Some(self.parse_expr(0)?)
19277        } else {
19278            None
19279        };
19280        Ok(crate::ast::OnConflictAction::Update {
19281            assignments,
19282            where_,
19283        })
19284    }
19285
19286    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
19287        let mut items = Vec::new();
19288        // v7.39 (round 341, V66) — PG's target list may be EMPTY
19289        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
19290        // answers one zero-column row per row of t, and a bare `SELECT`
19291        // answers a single zero-column row. SPG required at least one
19292        // item, so both were syntax errors. Recognised by the token that
19293        // follows — nothing that can start an expression appears here.
19294        if self.select_list_is_empty_here() {
19295            return Ok(items);
19296        }
19297        loop {
19298            items.push(self.parse_select_item()?);
19299            if matches!(self.peek(), Token::Comma) {
19300                self.advance();
19301            } else {
19302                break;
19303            }
19304        }
19305        Ok(items)
19306    }
19307
19308    /// Is the target list empty at this point — i.e. does the next token
19309    /// end the SELECT's item list rather than start an item?
19310    fn select_list_is_empty_here(&self) -> bool {
19311        match self.peek() {
19312            Token::From
19313            | Token::Where
19314            | Token::Group
19315            | Token::Having
19316            | Token::Order
19317            | Token::Limit
19318            | Token::Offset
19319            | Token::Semicolon
19320            | Token::RParen
19321            | Token::Union
19322            | Token::Except
19323            | Token::Eof => true,
19324            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
19325            // with unreserved keywords, so they arrive as plain idents.
19326            Token::Ident(s) => {
19327                s.eq_ignore_ascii_case("fetch")
19328                    || s.eq_ignore_ascii_case("window")
19329                    || s.eq_ignore_ascii_case("intersect")
19330            }
19331            _ => false,
19332        }
19333    }
19334
19335    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
19336        if matches!(self.peek(), Token::Star) {
19337            self.advance();
19338            return Ok(SelectItem::Wildcard);
19339        }
19340        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
19341        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
19342        // choke on the `*` ("expected identifier, got Star"). The lookahead is
19343        // `<ident> . *` with nothing binding tighter.
19344        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
19345            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19346                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
19347            {
19348                self.advance(); // qualifier
19349                self.advance(); // .
19350                self.advance(); // *
19351                return Ok(SelectItem::QualifiedWildcard(q));
19352            }
19353        }
19354        let start_tok = self.pos;
19355        let expr = self.parse_expr(0)?;
19356        let end_tok = self.consumed_pos();
19357        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
19358        // multi-column function returns into columns. Marked here and lowered in
19359        // `parse_bare_select`, where the FROM clause is in hand.
19360        if matches!(self.peek(), Token::Dot)
19361            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
19362        {
19363            self.advance(); // .
19364            self.advance(); // *
19365            return Ok(SelectItem::Expr {
19366                expr: Expr::FunctionCall {
19367                    name: "__record_expand".to_string(),
19368                    args: alloc::vec![expr],
19369                },
19370                alias: None,
19371            });
19372        }
19373        // v7.39.2 — MySQL lets a STRING name a projection item, with or
19374        // without `AS`: `SELECT 1 'x'`, `SELECT COUNT(*) 'total'`,
19375        // `SELECT 1 'a b'` (which is why one quotes it). SPG answered
19376        // `syntax error at or near "'x'"` to all of them.
19377        //
19378        // Only here, not in `parse_optional_alias`: that one also names
19379        // TABLES, and MySQL 9.7.2 refuses a string there — `FROM t 'ta'`
19380        // and `FROM t AS 'ta'` are both syntax errors, measured. And only
19381        // after the lexer's own rule has joined adjacent literals, or
19382        // `SELECT 'a' 'b'` would read as a literal aliased `b` where
19383        // MySQL answers the concatenation `ab`.
19384        if self.mysql_dialect {
19385            let at_as = matches!(self.peek(), Token::As)
19386                && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)));
19387            if at_as {
19388                self.advance();
19389            }
19390            if let Token::String(name) = self.peek().clone() {
19391                self.advance();
19392                return Ok(SelectItem::Expr {
19393                    expr,
19394                    alias: Some(name),
19395                });
19396            }
19397        }
19398        let alias = match self.parse_optional_alias()? {
19399            Some(a) => Some(a),
19400            None => self.mysql_item_label(&expr, start_tok, end_tok),
19401        };
19402        Ok(SelectItem::Expr { expr, alias })
19403    }
19404
19405    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
19406    /// carries no `AS`, filled in here so every downstream path reports it
19407    /// without knowing the rule. `None` leaves the item un-aliased, which is
19408    /// what a PG session always gets.
19409    ///
19410    /// Measured against MariaDB 11, three rules and no more:
19411    ///
19412    /// | item             | label      | why                          |
19413    /// |------------------|------------|------------------------------|
19414    /// | `lbl.a`          | `a`        | a column reports its name    |
19415    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
19416    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
19417    ///
19418    /// The third is why this lives in the parser at all: the label is the
19419    /// text the client WROTE, down to the spacing, so it cannot be printed
19420    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
19421    ///
19422    /// Comments survive, and that is right: through a `mariadb` CLI both
19423    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
19424    /// CLIENT stripping the comment before it sends. Asked over the raw
19425    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
19426    /// produces.
19427    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
19428        if !self.mysql_dialect {
19429            return None;
19430        }
19431        match expr {
19432            // A column already reports its own name downstream; naming it
19433            // again here would only re-state the qualifier the label drops.
19434            Expr::Column(_) => None,
19435            // v7.39.3 — `SELECT 'a' 'b'` is ONE literal whose value is
19436            // `ab`, and MySQL 9.7.2 names the column `a`: the label is
19437            // the first segment as written, not the joined value
19438            // (measured). The lexer logs where it joined them.
19439            Expr::Literal(Literal::String(v)) => Some(
19440                self.merged_first_len(start_tok)
19441                    .and_then(|n| v.get(..n))
19442                    .map_or_else(|| v.clone(), String::from),
19443            ),
19444            _ => self.source_span(start_tok, end_tok).map(str::to_string),
19445        }
19446    }
19447
19448    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
19449    /// consumed VALUES keyword. Each row lowers to a constant SELECT
19450    /// with PG's default column1..columnN names; subsequent rows
19451    /// chain as UNION ALL peers. Shared by the FROM-position
19452    /// `( VALUES … )` arm and the top-level bare VALUES statement.
19453    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
19454        let mut row_selects: Vec<SelectStatement> = Vec::new();
19455        loop {
19456            if !matches!(self.peek(), Token::LParen) {
19457                return Err(self.err(alloc::format!(
19458                    "expected '(' to start a VALUES row, got {:?}",
19459                    self.peek()
19460                )));
19461            }
19462            self.advance(); // (
19463            let mut items: Vec<SelectItem> = Vec::new();
19464            loop {
19465                let expr = self.parse_expr(0)?;
19466                items.push(SelectItem::Expr {
19467                    expr,
19468                    alias: Some(alloc::format!("column{}", items.len() + 1)),
19469                });
19470                match self.peek() {
19471                    Token::Comma => {
19472                        self.advance();
19473                    }
19474                    Token::RParen => break,
19475                    other => {
19476                        return Err(self.err(alloc::format!(
19477                            "expected ',' or ')' in VALUES row, got {other:?}"
19478                        )));
19479                    }
19480                }
19481            }
19482            self.advance(); // )
19483            row_selects.push(SelectStatement {
19484                locking: None,
19485                ctes: Vec::new(),
19486                distinct: false,
19487                distinct_on: Vec::new(),
19488                items,
19489                from: None,
19490                where_: None,
19491                group_by: None,
19492                group_by_all: false,
19493                having: None,
19494                unions: Vec::new(),
19495                order_by: Vec::new(),
19496                limit: None,
19497                offset: None,
19498                limit_with_ties: false,
19499                window_check_exprs: Vec::new(),
19500            });
19501            if matches!(self.peek(), Token::Comma) {
19502                self.advance();
19503                continue;
19504            }
19505            break;
19506        }
19507        let mut head = row_selects.remove(0);
19508        head.unions = row_selects
19509            .into_iter()
19510            .map(|s| (UnionKind::All, s))
19511            .collect();
19512        Ok(head)
19513    }
19514
19515    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
19516        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
19517        // children. It was read as a table NAMED `only`, so the query
19518        // failed on `relation "only" does not exist`.
19519        //
19520        // v7.39 (round 644) — and it is no longer a no-op. Round 621
19521        // absorbed the keyword, reasoning that SPG's children are
19522        // separate relations a plain scan does not descend into, so ONLY
19523        // already described the scan. That stopped being true when a
19524        // partition parent started unioning its children: measured,
19525        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
19526        // where PG answers 0. The flag is carried now.
19527        let mut only = false;
19528        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
19529            && matches!(
19530                self.tokens.get(self.pos + 1),
19531                Some(Token::Ident(_) | Token::QuotedIdent(_))
19532            )
19533        {
19534            only = true;
19535            self.advance();
19536        }
19537        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
19538        // for these SRFs the keyword is noise at parse time: the
19539        // join executor already substitutes outer-column references
19540        // into unnest_expr / generate_series_args per outer row
19541        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
19542        // licences the correlation even without the keyword. Absorb
19543        // it and fall through to the SRF arms below.
19544        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
19545        // just the four builtin SRFs: a user set-returning function on a JOIN's
19546        // right side is the whole point of LATERAL. The keyword stays noise at
19547        // parse time — the join executor substitutes the outer row into the
19548        // call's arguments per outer row.
19549        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19550            && matches!(
19551                self.tokens.get(self.pos + 1),
19552                // The json_each family has its OWN `LATERAL …` arm below, which
19553                // needs to see the keyword — absorbing it here would send those
19554                // calls down the generic table-function channel instead.
19555                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
19556            )
19557            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19558        {
19559            self.advance(); // LATERAL
19560        }
19561        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
19562        // set-returning function whose argument may reference a
19563        // preceding FROM item. We rewrite this to
19564        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
19565        // AS __srf__) AS <alias>` so the existing LATERAL subquery
19566        // executor handles per-outer-row evaluation and the
19567        // SRF-primary jsonb_each_text path handles the inner
19568        // materialisation. Sentori 0067 backfill is the dogfood
19569        // shape.
19570        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19571            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
19572            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19573        {
19574            self.advance(); // LATERAL
19575            let each_fn = match self.peek() {
19576                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19577                _ => unreachable!(),
19578            };
19579            self.advance(); // jsonb_each[_text] / json_each[_text]
19580            self.advance(); // (
19581            let arg = self.parse_expr(0)?;
19582            if !matches!(self.peek(), Token::RParen) {
19583                return Err(self.err(alloc::format!(
19584                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
19585                    self.peek()
19586                )));
19587            }
19588            self.advance();
19589            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19590            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19591            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
19592            //               FROM jsonb_each_text(<arg>) AS __srf__
19593            // PG's `AS kv(key, value)` column-alias list maps
19594            // positions to names; default to (key, value) when
19595            // omitted (matching the SRF's natural column names).
19596            let srf_alias = "__srf__".to_string();
19597            let key_alias = column_aliases
19598                .first()
19599                .cloned()
19600                .unwrap_or_else(|| "key".to_string());
19601            let value_alias = column_aliases
19602                .get(1)
19603                .cloned()
19604                .unwrap_or_else(|| "value".to_string());
19605            let inner_select = crate::ast::SelectStatement {
19606                locking: None,
19607                ctes: Vec::new(),
19608                distinct: false,
19609                distinct_on: Vec::new(),
19610                items: alloc::vec![
19611                    crate::ast::SelectItem::Expr {
19612                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19613                            qualifier: Some(srf_alias.clone()),
19614                            name: "key".to_string(),
19615                        }),
19616                        alias: Some(key_alias),
19617                    },
19618                    crate::ast::SelectItem::Expr {
19619                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19620                            qualifier: Some(srf_alias.clone()),
19621                            name: "value".to_string(),
19622                        }),
19623                        alias: Some(value_alias),
19624                    },
19625                ],
19626                from: Some(crate::ast::FromClause {
19627                    primary: TableRef {
19628                        name: srf_alias.clone(),
19629                        alias: Some(srf_alias.clone()),
19630                        only: false,
19631                        as_of_segment: None,
19632                        unnest_expr: None,
19633                        unnest_column_aliases: Vec::new(),
19634                        with_ordinality: false,
19635                        generate_series_args: None,
19636                        lateral_subquery: None,
19637                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19638                        table_fn_call: None,
19639                        rows_from: None,
19640                        json_table: None,
19641                        scalar_fn_item: false,
19642                    },
19643                    joins: Vec::new(),
19644                }),
19645                where_: None,
19646                group_by: None,
19647                group_by_all: false,
19648                having: None,
19649                unions: Vec::new(),
19650                order_by: Vec::new(),
19651                limit: None,
19652                offset: None,
19653                limit_with_ties: false,
19654                window_check_exprs: Vec::new(),
19655            };
19656            return Ok(TableRef {
19657                name: alias.clone(),
19658                alias: Some(alias),
19659                only: false,
19660                as_of_segment: None,
19661                unnest_expr: None,
19662                unnest_column_aliases: Vec::new(),
19663                with_ordinality: false,
19664                generate_series_args: None,
19665                lateral_subquery: Some(Box::new(inner_select)),
19666                jsonb_each_text_arg: None,
19667                table_fn_call: None,
19668                rows_from: None,
19669                json_table: None,
19670                scalar_fn_item: false,
19671            });
19672        }
19673        // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
19674        // without an explicit `LATERAL` keyword is the same shape
19675        // PG accepts (SRF naturally licences lateral correlation).
19676        // We mirror the LATERAL rewrite when the argument syntactic-
19677        // ally references an outer column (Column { qualifier:
19678        // Some(_), … }). For simplicity we apply the rewrite
19679        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
19680        // in the FROM-list — caller-side join parsing positions
19681        // this peek correctly.
19682        // (Implementation note: detection lives below; the LATERAL
19683        // branch above already covers the explicit form; the bare
19684        // form falls through to the plain SRF arm and the engine
19685        // treats it as a constant-arg SRF if no outer reference is
19686        // present.)
19687        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
19688        // table. Detect at the head so it claims precedence over
19689        // every other table-ref shape (unnest / generate_series /
19690        // bare ident); the lateral subquery itself follows the
19691        // regular SELECT grammar.
19692        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
19693        // t(cols)`. Each row lowers to a constant SELECT with PG's
19694        // default column1..columnN names; subsequent rows chain as
19695        // UNION ALL peers. The result rides the derived-table
19696        // lateral_subquery channel — zero executor work.
19697        if matches!(self.peek(), Token::LParen)
19698            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
19699        {
19700            self.advance(); // (
19701            self.advance(); // VALUES
19702            let head = self.parse_values_rows_body()?;
19703            if !matches!(self.peek(), Token::RParen) {
19704                return Err(self.err(alloc::format!(
19705                    "expected ')' after VALUES list, got {:?}",
19706                    self.peek()
19707                )));
19708            }
19709            self.advance();
19710            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19711            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
19712            return Ok(TableRef {
19713                name,
19714                alias: alias_ident,
19715                only: false,
19716                as_of_segment: None,
19717                unnest_expr: None,
19718                unnest_column_aliases: column_aliases,
19719                with_ordinality: false,
19720                generate_series_args: None,
19721                lateral_subquery: Some(Box::new(head)),
19722                jsonb_each_text_arg: None,
19723                table_fn_call: None,
19724                rows_from: None,
19725                json_table: None,
19726                scalar_fn_item: false,
19727            });
19728        }
19729        // v7.37.17 (17.6 siblings) — plain derived table:
19730        // `FROM ( SELECT … ) [AS] alias`. Rides the same
19731        // lateral_subquery channel the explicit LATERAL form uses —
19732        // an uncorrelated inner SELECT executes identically. The
19733        // inner parse carries UNION tails (they live on
19734        // SelectStatement.unions).
19735        // v7.37 D.20 — the derived-table inner may itself be a
19736        // parenthesized set-operation group (`FROM ((SELECT…) UNION
19737        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
19738        // bare `(SELECT …)`. parse_one_statement already routes a leading
19739        // `(` set-op group (its LParen arm) and a leading WITH
19740        // (parse_with_cte_then_select), so widen the second-token gate to
19741        // Select | LParen | WITH.
19742        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
19743        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
19744        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
19745        // has existed since the shorthand landed and `parse_bare_select`
19746        // already routes it ("valid anywhere a SELECT head is"); what was
19747        // missing is this second-token gate, and the CTE body's dispatch
19748        // below. Round 868 found both by putting the shorthand in a
19749        // subquery — the top-level forms had been the only ones tested.
19750        if matches!(self.peek(), Token::LParen)
19751            && (matches!(
19752                self.tokens.get(self.pos + 1),
19753                Some(Token::Select | Token::LParen | Token::Table)
19754            ) || matches!(self.tokens.get(self.pos + 1),
19755                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
19756        {
19757            self.advance(); // (
19758            let inner = match self.parse_one_statement()? {
19759                Statement::Select(s) => s,
19760                other => {
19761                    return Err(self.err(alloc::format!(
19762                        "expected SELECT inside derived table ( … ), got {other:?}"
19763                    )));
19764                }
19765            };
19766            if !matches!(self.peek(), Token::RParen) {
19767                return Err(self.err(alloc::format!(
19768                    "expected ')' after derived-table subquery, got {:?}",
19769                    self.peek()
19770                )));
19771            }
19772            self.advance();
19773            // `AS t(a, b)` column-alias list rides the
19774            // unnest_column_aliases field (same positional-rename
19775            // contract the unnest SRFs use).
19776            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19777            let name = alias_ident
19778                .clone()
19779                .unwrap_or_else(|| "subquery".to_string());
19780            return Ok(TableRef {
19781                name,
19782                alias: alias_ident,
19783                only: false,
19784                as_of_segment: None,
19785                unnest_expr: None,
19786                unnest_column_aliases: column_aliases,
19787                with_ordinality: false,
19788                generate_series_args: None,
19789                lateral_subquery: Some(Box::new(inner)),
19790                jsonb_each_text_arg: None,
19791                table_fn_call: None,
19792                rows_from: None,
19793                json_table: None,
19794                scalar_fn_item: false,
19795            });
19796        }
19797        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19798            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19799        {
19800            self.advance(); // LATERAL
19801            self.advance(); // (
19802            // Parse the inner SELECT.
19803            let inner = match self.parse_one_statement()? {
19804                Statement::Select(s) => s,
19805                other => {
19806                    return Err(self.err(alloc::format!(
19807                        "expected SELECT inside LATERAL ( … ), got {other:?}"
19808                    )));
19809                }
19810            };
19811            if !matches!(self.peek(), Token::RParen) {
19812                return Err(self.err(alloc::format!(
19813                    "expected ')' after LATERAL subquery, got {:?}",
19814                    self.peek()
19815                )));
19816            }
19817            self.advance();
19818            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19819            // `(VALUES …) t(g)` derived table round-trips through view-body
19820            // Display, which renders on the lateral_subquery channel).
19821            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19822            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19823            return Ok(TableRef {
19824                name,
19825                alias: alias_ident,
19826                only: false,
19827                as_of_segment: None,
19828                unnest_expr: None,
19829                unnest_column_aliases: column_aliases,
19830                with_ordinality: false,
19831                generate_series_args: None,
19832                lateral_subquery: Some(Box::new(inner)),
19833                jsonb_each_text_arg: None,
19834                table_fn_call: None,
19835                rows_from: None,
19836                json_table: None,
19837                scalar_fn_item: false,
19838            });
19839        }
19840        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19841        // function as a FROM item. Emits one row per (key, value)
19842        // pair in the JSONB object argument as TEXT columns. May
19843        // be wrapped in CROSS JOIN LATERAL when the argument
19844        // references a preceding FROM item (sentori migration
19845        // 0067 backfill shape: `CROSS JOIN LATERAL
19846        // jsonb_each_text(t.json_col) AS kv(key, value)`).
19847        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19848            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19849        {
19850            let each_fn = match self.peek() {
19851                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19852                _ => unreachable!(),
19853            };
19854            self.advance(); // jsonb_each[_text] / json_each[_text]
19855            self.advance(); // (
19856            let arg = self.parse_expr(0)?;
19857            if !matches!(self.peek(), Token::RParen) {
19858                return Err(self.err(alloc::format!(
19859                    "expected ')' after {each_fn}() argument, got {:?}",
19860                    self.peek()
19861                )));
19862            }
19863            self.advance();
19864            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19865            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19866            return Ok(TableRef {
19867                name,
19868                alias: alias_ident,
19869                only: false,
19870                as_of_segment: None,
19871                unnest_expr: None,
19872                // `AS t(k, v)` renames key/value positionally, same as the
19873                // LATERAL-position form already does.
19874                unnest_column_aliases: column_aliases,
19875                with_ordinality: false,
19876                generate_series_args: None,
19877                lateral_subquery: None,
19878                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19879                table_fn_call: None,
19880                rows_from: None,
19881                json_table: None,
19882                scalar_fn_item: false,
19883            });
19884        }
19885        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19886        // (+ json_ variants) — record-returning JSON functions with a
19887        // column-definition list. Desugar to a derived table that
19888        // projects each declared column from the JSON via `->>` + a cast,
19889        // over `jsonb_array_elements(J)` for the *set (per-element) form.
19890        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19891            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19892        {
19893            return self.parse_json_to_record_from();
19894        }
19895        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19896        // row is a text[] of capture groups, so it cannot desugar to unnest
19897        // (that would flatten the array). Wrap it as a derived table
19898        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19899        // SRF path already emits one text[] row per match. PG names the column
19900        // `regexp_matches`; an `AS a(col)` alias overrides it.
19901        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19902                if s.eq_ignore_ascii_case("regexp_matches"))
19903            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19904        {
19905            self.advance(); // fn name
19906            self.advance(); // (
19907            let mut fn_args: Vec<Expr> = Vec::new();
19908            loop {
19909                fn_args.push(self.parse_expr(0)?);
19910                if matches!(self.peek(), Token::Comma) {
19911                    self.advance();
19912                    continue;
19913                }
19914                break;
19915            }
19916            if !matches!(self.peek(), Token::RParen) {
19917                return Err(self.err(alloc::format!(
19918                    "expected ')' after regexp_matches() arguments, got {:?}",
19919                    self.peek()
19920                )));
19921            }
19922            self.advance();
19923            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19924            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19925            // it, so it died on the `with` token while every other table function
19926            // accepted it.
19927            let with_ordinality = self.absorb_with_ordinality();
19928            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19929            let table_alias = alias_ident
19930                .clone()
19931                .unwrap_or_else(|| "regexp_matches".to_string());
19932            // PG names a single-column function's output column after the ALIAS
19933            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19934            // `m` reads as that column and not as a whole-row composite. Naming
19935            // it after the function regardless made `SELECT m[1] FROM … AS m`
19936            // subscript a record.
19937            let col_name = column_aliases
19938                .first()
19939                .cloned()
19940                .or_else(|| alias_ident.clone())
19941                .unwrap_or_else(|| "regexp_matches".to_string());
19942            let inner = crate::ast::SelectStatement {
19943                locking: None,
19944                ctes: Vec::new(),
19945                distinct: false,
19946                distinct_on: Vec::new(),
19947                items: alloc::vec![SelectItem::Expr {
19948                    expr: Expr::FunctionCall {
19949                        name: "regexp_matches".to_string(),
19950                        args: fn_args,
19951                    },
19952                    alias: Some(col_name),
19953                }],
19954                from: None,
19955                where_: None,
19956                group_by: None,
19957                group_by_all: false,
19958                having: None,
19959                unions: Vec::new(),
19960                order_by: Vec::new(),
19961                limit: None,
19962                offset: None,
19963                limit_with_ties: false,
19964                window_check_exprs: Vec::new(),
19965            };
19966            return Ok(TableRef {
19967                name: table_alias.clone(),
19968                alias: Some(table_alias),
19969                only: false,
19970                as_of_segment: None,
19971                unnest_expr: None,
19972                unnest_column_aliases: column_aliases,
19973                with_ordinality,
19974                generate_series_args: None,
19975                lateral_subquery: Some(Box::new(inner)),
19976                jsonb_each_text_arg: None,
19977                table_fn_call: None,
19978                rows_from: None,
19979                json_table: None,
19980                // regexp_matches returns text[], a base type: `SELECT m FROM
19981                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19982                scalar_fn_item: true,
19983            });
19984        }
19985        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19986        // / json_ variants as a FROM item. Rewritten into
19987        // `unnest(<same fn>(<expr>))`: the scalar form returns the
19988        // elements as a TEXT array, and the existing unnest SRF path
19989        // materialises one row per element. PG's natural column name
19990        // is `value`; an `AS a(col)` column-alias list overrides it.
19991        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19992                if s.eq_ignore_ascii_case("jsonb_array_elements")
19993                    || s.eq_ignore_ascii_case("json_array_elements")
19994                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19995                    || s.eq_ignore_ascii_case("json_array_elements_text")
19996                    || s.eq_ignore_ascii_case("jsonb_object_keys")
19997                    || s.eq_ignore_ascii_case("json_object_keys")
19998                    || s.eq_ignore_ascii_case("jsonb_path_query")
19999                    || s.eq_ignore_ascii_case("json_path_query")
20000                    || s.eq_ignore_ascii_case("generate_subscripts")
20001                    || s.eq_ignore_ascii_case("string_to_table")
20002                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
20003            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20004        {
20005            let fn_name = match self.peek() {
20006                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20007                _ => unreachable!(),
20008            };
20009            self.advance(); // fn name
20010            self.advance(); // (
20011            let mut fn_args: Vec<Expr> = Vec::new();
20012            loop {
20013                fn_args.push(self.parse_expr(0)?);
20014                if matches!(self.peek(), Token::Comma) {
20015                    self.advance();
20016                    continue;
20017                }
20018                break;
20019            }
20020            if !matches!(self.peek(), Token::RParen) {
20021                return Err(self.err(alloc::format!(
20022                    "expected ')' after {fn_name}() arguments, got {:?}",
20023                    self.peek()
20024                )));
20025            }
20026            self.advance();
20027            let with_ordinality = self.absorb_with_ordinality();
20028            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
20029            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20030            // PG's natural column name: the array-elements SRFs
20031            // declare an OUT parameter `value`; jsonb_object_keys
20032            // and generate_subscripts have none, so the column is
20033            // named after the function. A bare table alias on a
20034            // single-column SRF renames the column too (PG: `FROM
20035            // generate_subscripts(a, 1) AS s` projects column s) —
20036            // except for the OUT-parameter SRFs, whose column stays
20037            // `value` under a bare alias.
20038            let natural_col = if fn_name.ends_with("_array_elements")
20039                || fn_name.ends_with("_array_elements_text")
20040            {
20041                "value".to_string()
20042            } else {
20043                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
20044            };
20045            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
20046            // Keep any further entries — the second names the
20047            // ordinality column under WITH ORDINALITY.
20048            srf_cols.extend(column_aliases.into_iter().skip(1));
20049            // The *_to_table SRFs are row-streams over the existing
20050            // *_to_array scalars — map the call target; the display
20051            // name (alias / column defaults) keeps the SRF spelling.
20052            let call_name = match fn_name.as_str() {
20053                "string_to_table" => "string_to_array".to_string(),
20054                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
20055                _ => fn_name,
20056            };
20057            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
20058            // preceding FROM item (bare or qualified column) is correlated;
20059            // route it through the per-outer-row lateral channel.
20060            let expr = crate::ast::Expr::FunctionCall {
20061                name: call_name,
20062                args: fn_args,
20063            };
20064            let correlated = Self::expr_has_any_column(&expr);
20065            let tref = TableRef {
20066                name,
20067                alias: alias_ident,
20068                only: false,
20069                as_of_segment: None,
20070                unnest_expr: Some(Box::new(expr)),
20071                unnest_column_aliases: srf_cols,
20072                with_ordinality,
20073                generate_series_args: None,
20074                lateral_subquery: None,
20075                jsonb_each_text_arg: None,
20076                table_fn_call: None,
20077                rows_from: None,
20078                json_table: None,
20079                // Each of these returns a BASE type (jsonb / text / int), so the item's
20080                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
20081                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
20082                scalar_fn_item: !with_ordinality,
20083            };
20084            return Ok(if correlated {
20085                Self::wrap_correlated_srf(tref)
20086            } else {
20087                tref
20088            });
20089        }
20090        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
20091        // explicit parallel-zip syntax. Each entry lowers to its
20092        // array-returning scalar form (unnest(x) → x itself; the
20093        // FROM-SRF rewrite family → their scalar array calls) and
20094        // the list rides the multi-arg unnest zip channel:
20095        // NULL-padded to the longest, WITH ORDINALITY appends the
20096        // counter. generate_series has no scalar array form and
20097        // errors honestly.
20098        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
20099            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
20100            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
20101        {
20102            self.advance(); // ROWS
20103            self.advance(); // FROM
20104            self.advance(); // (
20105            let mut entries: Vec<Expr> = Vec::new();
20106            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
20107            // Used only when some entry has no array form.
20108            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
20109            loop {
20110                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
20111                if !matches!(self.peek(), Token::LParen) {
20112                    return Err(self.err(alloc::format!(
20113                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
20114                        self.peek()
20115                    )));
20116                }
20117                self.advance();
20118                let mut fn_args: Vec<Expr> = Vec::new();
20119                if !matches!(self.peek(), Token::RParen) {
20120                    loop {
20121                        fn_args.push(self.parse_expr(0)?);
20122                        if matches!(self.peek(), Token::Comma) {
20123                            self.advance();
20124                            continue;
20125                        }
20126                        break;
20127                    }
20128                }
20129                if !matches!(self.peek(), Token::RParen) {
20130                    return Err(self.err(alloc::format!(
20131                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
20132                        self.peek()
20133                    )));
20134                }
20135                self.advance();
20136                let entry = match fn_name.as_str() {
20137                    "unnest" => {
20138                        if fn_args.len() != 1 {
20139                            return Err(
20140                                self.err("unnest inside ROWS FROM takes exactly one array".into())
20141                            );
20142                        }
20143                        fn_args.pop().expect("len checked")
20144                    }
20145                    "jsonb_array_elements"
20146                    | "json_array_elements"
20147                    | "jsonb_array_elements_text"
20148                    | "json_array_elements_text"
20149                    | "jsonb_object_keys"
20150                    | "json_object_keys"
20151                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
20152                        name: fn_name,
20153                        args: fn_args,
20154                    },
20155                    "string_to_table" => crate::ast::Expr::FunctionCall {
20156                        name: "string_to_array".to_string(),
20157                        args: fn_args,
20158                    },
20159                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
20160                        name: "regexp_split_to_array".to_string(),
20161                        args: fn_args,
20162                    },
20163                    // v7.39 (read01 round 74) — an SRF with no array form
20164                    // (`generate_series`, a user `RETURNS SETOF` function) has no
20165                    // scalar expression to zip, so the WHOLE list switches to the
20166                    // rows_from channel, which runs each function and zips the
20167                    // rows themselves. The all-array case keeps the old lowering:
20168                    // it is well-trodden and this must not disturb it.
20169                    _ => {
20170                        generic.push((fn_name, fn_args));
20171                        if matches!(self.peek(), Token::Comma) {
20172                            self.advance();
20173                            continue;
20174                        }
20175                        break;
20176                    }
20177                };
20178                generic.push((
20179                    // The array-able entries carry their lowered expr along, so a
20180                    // MIXED list still works: the engine sees the scalar array
20181                    // form and unnests it.
20182                    "__array".to_string(),
20183                    alloc::vec![entry.clone()],
20184                ));
20185                entries.push(entry);
20186                if matches!(self.peek(), Token::Comma) {
20187                    self.advance();
20188                    continue;
20189                }
20190                break;
20191            }
20192            if !matches!(self.peek(), Token::RParen) {
20193                return Err(self.err(alloc::format!(
20194                    "expected ')' to close ROWS FROM, got {:?}",
20195                    self.peek()
20196                )));
20197            }
20198            self.advance();
20199            let with_ordinality = self.absorb_with_ordinality();
20200            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20201            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
20202            // v7.39 (read01 round 74) — some entry had no array form, so the whole
20203            // list rides the generic channel.
20204            if generic.iter().any(|(n, _)| n != "__array") {
20205                let correlated = generic
20206                    .iter()
20207                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
20208                let tref = TableRef {
20209                    name,
20210                    alias: alias_ident,
20211                    only: false,
20212                    as_of_segment: None,
20213                    unnest_expr: None,
20214                    unnest_column_aliases,
20215                    with_ordinality,
20216                    generate_series_args: None,
20217                    lateral_subquery: None,
20218                    jsonb_each_text_arg: None,
20219                    table_fn_call: None,
20220                    rows_from: Some(generic),
20221                    json_table: None,
20222                    scalar_fn_item: false,
20223                };
20224                return Ok(if correlated {
20225                    Self::wrap_correlated_srf(tref)
20226                } else {
20227                    tref
20228                });
20229            }
20230            let correlated = entries.iter().any(Self::expr_has_any_column);
20231            let expr = if entries.len() == 1 {
20232                entries.pop().expect("len checked")
20233            } else {
20234                crate::ast::Expr::FunctionCall {
20235                    name: "__unnest_zip".to_string(),
20236                    args: entries,
20237                }
20238            };
20239            let tref = TableRef {
20240                name,
20241                alias: alias_ident,
20242                only: false,
20243                as_of_segment: None,
20244                unnest_expr: Some(Box::new(expr)),
20245                unnest_column_aliases,
20246                with_ordinality,
20247                generate_series_args: None,
20248                lateral_subquery: None,
20249                jsonb_each_text_arg: None,
20250                table_fn_call: None,
20251                rows_from: None,
20252                json_table: None,
20253                scalar_fn_item: false,
20254            };
20255            return Ok(if correlated {
20256                Self::wrap_correlated_srf(tref)
20257            } else {
20258                tref
20259            });
20260        }
20261        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
20262        // source. Detect at the head before the bare-ident fallback;
20263        // unnest is not a reserved token.
20264        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
20265            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20266        {
20267            self.advance(); // unnest
20268            self.advance(); // (
20269            let mut srf_args = alloc::vec![self.parse_expr(0)?];
20270            while matches!(self.peek(), Token::Comma) {
20271                self.advance();
20272                srf_args.push(self.parse_expr(0)?);
20273            }
20274            if !matches!(self.peek(), Token::RParen) {
20275                return Err(self.err(alloc::format!(
20276                    "expected ')' after unnest() argument, got {:?}",
20277                    self.peek()
20278                )));
20279            }
20280            self.advance();
20281            // Multi-arg unnest(a, b, …) zips the arrays in
20282            // parallel, NULL-padding to the longest (PG's ROWS
20283            // FROM shorthand). Lower onto the unnest channel as an
20284            // internal marker call the executors unpack.
20285            let expr = if srf_args.len() == 1 {
20286                srf_args.pop().expect("len checked")
20287            } else {
20288                crate::ast::Expr::FunctionCall {
20289                    name: "__unnest_zip".to_string(),
20290                    args: srf_args,
20291                }
20292            };
20293            let with_ordinality = self.absorb_with_ordinality();
20294            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20295            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
20296            let correlated = Self::expr_has_any_column(&expr);
20297            let tref = TableRef {
20298                name,
20299                alias: alias_ident,
20300                only: false,
20301                as_of_segment: None,
20302                unnest_expr: Some(Box::new(expr)),
20303                unnest_column_aliases,
20304                with_ordinality,
20305                generate_series_args: None,
20306                lateral_subquery: None,
20307                jsonb_each_text_arg: None,
20308                table_fn_call: None,
20309                rows_from: None,
20310                json_table: None,
20311                scalar_fn_item: false,
20312            };
20313            return Ok(if correlated {
20314                Self::wrap_correlated_srf(tref)
20315            } else {
20316                tref
20317            });
20318        }
20319        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
20320        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
20321        // generic table-fn arg parser can't read), so it is intercepted
20322        // here BEFORE the generic dispatch. The doc expr may reference
20323        // outer columns (implicit LATERAL) — same correlated-wrap rule.
20324        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20325                if s.eq_ignore_ascii_case("json_table"))
20326            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20327        {
20328            let tref = self.parse_json_table_ref()?;
20329            let correlated = tref
20330                .json_table
20331                .as_deref()
20332                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
20333            return Ok(if correlated {
20334                Self::wrap_correlated_srf(tref)
20335            } else {
20336                tref
20337            });
20338        }
20339        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
20340        // functions dispatched by name (`pg_partition_tree('t')`,
20341        // `pg_partition_ancestors('t')`). Same head-detection shape as
20342        // unnest; the engine executor owns the row shape per function.
20343        // v7.39 (read01 round 65) — and a USER function in FROM position
20344        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
20345        // (generate_series / unnest / the json_each family) keep it — their arms
20346        // sit further down, so they are excluded here by name rather than by
20347        // ordering. Anything else that is an ident followed by `(` is a table
20348        // function; the engine executor decides whether it is a builtin, a
20349        // set-returning user function, or an error.
20350        // 7.38.1 S5.1 — pg_dump spells its table functions
20351        // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
20352        // strip the pg_catalog prefix here so the same head-detection
20353        // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
20354        // meaning.
20355        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
20356            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
20357            && matches!(
20358                self.tokens.get(self.pos + 2),
20359                Some(Token::Ident(_) | Token::QuotedIdent(_))
20360            )
20361            && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
20362        {
20363            self.advance(); // pg_catalog
20364            self.advance(); // .
20365        }
20366        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20367                if !s.eq_ignore_ascii_case("generate_series")
20368                    && !s.eq_ignore_ascii_case("unnest")
20369                    && !is_json_each_name(s))
20370            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20371        {
20372            // Body out-of-line — this parse sits on the FROM/subquery
20373            // recursion chain (debug frame-cliff discipline).
20374            // v7.39 (read01 round 69) — a call whose arguments reference an outer
20375            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
20376            // outer row, so it rides the lateral channel. Same rule the unnest
20377            // arm uses.
20378            let tref = self.parse_table_fn_ref()?;
20379            let correlated = tref
20380                .table_fn_call
20381                .as_deref()
20382                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
20383            return Ok(if correlated {
20384                Self::wrap_correlated_srf(tref)
20385            } else {
20386                tref
20387            });
20388        }
20389        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
20390        // [, step])` set-returning source. Same shape as unnest:
20391        // detect at the head, parse the comma-separated arg list,
20392        // dispatch downstream through the engine's set-returning
20393        // path. Supports integer triplets (mailrs's `WITH row_no AS
20394        // (SELECT * FROM generate_series(1, N))` pattern) and
20395        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
20396        // date-range iteration pattern, which pre-3.10 had no
20397        // direct equivalent in SPG).
20398        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
20399            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20400        {
20401            self.advance(); // generate_series
20402            self.advance(); // (
20403            let mut args: Vec<Expr> = Vec::new();
20404            loop {
20405                args.push(self.parse_expr(0)?);
20406                if matches!(self.peek(), Token::Comma) {
20407                    self.advance();
20408                    continue;
20409                }
20410                break;
20411            }
20412            if !matches!(self.peek(), Token::RParen) {
20413                return Err(self.err(alloc::format!(
20414                    "expected ')' after generate_series() arguments, got {:?}",
20415                    self.peek()
20416                )));
20417            }
20418            self.advance();
20419            if args.len() < 2 || args.len() > 3 {
20420                return Err(self.err(alloc::format!(
20421                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
20422                    args.len()
20423                )));
20424            }
20425            let with_ordinality = self.absorb_with_ordinality();
20426            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
20427            let name = alias_ident
20428                .clone()
20429                .unwrap_or_else(|| "generate_series".to_string());
20430            let correlated = args.iter().any(Self::expr_has_any_column);
20431            let tref = TableRef {
20432                name,
20433                alias: alias_ident,
20434                only: false,
20435                as_of_segment: None,
20436                unnest_expr: None,
20437                unnest_column_aliases: column_aliases,
20438                with_ordinality,
20439                generate_series_args: Some(args),
20440                lateral_subquery: None,
20441                jsonb_each_text_arg: None,
20442                table_fn_call: None,
20443                rows_from: None,
20444                json_table: None,
20445                scalar_fn_item: false,
20446            };
20447            return Ok(if correlated {
20448                Self::wrap_correlated_srf(tref)
20449            } else {
20450                tref
20451            });
20452        }
20453        // v7.16.2 — preserve information_schema / pg_catalog
20454        // qualifiers (mailrs round-10 A.3). The generic
20455        // `expect_ident_like` strip silently drops the schema;
20456        // we want the engine to recognise these PG meta tables
20457        // and synthesise rows from the live catalog. Produce a
20458        // synthetic name (`__spg_info_columns` etc.) so the
20459        // engine's SELECT-side router can dispatch without
20460        // clashing with any user-defined `columns` table.
20461        let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
20462            (synth, Some(orig))
20463        } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
20464            (synth, Some(orig))
20465        } else {
20466            (self.expect_ident_like()?, None)
20467        };
20468        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
20469        // time-travel clause. Parse BEFORE the alias so the
20470        // alias can still ride at the tail (`tbl AS OF SEGMENT
20471        // '5' alias`). `AS` is a reserved keyword token, while
20472        // `OF` and `SEGMENT` are bare idents.
20473        let as_of_segment = if matches!(self.peek(), Token::As)
20474            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
20475        {
20476            self.advance(); // AS
20477            self.advance(); // OF
20478            let kw = match self.peek().clone() {
20479                Token::Ident(s) | Token::QuotedIdent(s) => s,
20480                other => {
20481                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
20482                }
20483            };
20484            if !kw.eq_ignore_ascii_case("segment") {
20485                return Err(self.err(format!(
20486                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
20487                )));
20488            }
20489            self.advance();
20490            // Segment id literal — accept either a string or
20491            // integer for operator ergonomics.
20492            let id = match self.advance() {
20493                Token::String(s) => s
20494                    .parse::<u32>()
20495                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20496                Token::Integer(n) => u32::try_from(n)
20497                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20498                other => {
20499                    return Err(self.err(format!(
20500                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
20501                    )));
20502                }
20503            };
20504            Some(id)
20505        } else {
20506            None
20507        };
20508        // TABLESAMPLE is not a reserved token — keep the bare-ident
20509        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
20510        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
20511        {
20512            None
20513        } else {
20514            self.parse_optional_alias()?
20515        };
20516        // r1052 — a catalog name rewritten to its synthetic form keeps
20517        // the WRITTEN name as the relation's alias, so `pg_cast.oid`
20518        // still binds after `pg_cast` became `__spg_pg_cast`. PG
20519        // semantics: the visible name of `pg_catalog.pg_cast` IS
20520        // `pg_cast`. Without this, every table-name-qualified column
20521        // on a synthesised catalog answered "missing FROM-clause
20522        // entry" — which is the wall pg_dump hit on its first
20523        // pg_proc/pg_cast query.
20524        let alias = match (&alias, &meta_original) {
20525            (None, Some(orig)) if *orig != name => Some(orig.clone()),
20526            _ => alias,
20527        };
20528        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
20529        // (PG grammar). BERNOULLI lowers to a per-row
20530        // `random() < p/100` conjunct on the enclosing SELECT's
20531        // WHERE — exact row-level Bernoulli semantics. SYSTEM
20532        // shares the lowering: SPG has no page structure to
20533        // sample, and the row-level form returns the same expected
20534        // fraction. REPEATABLE(seed) promises a deterministic
20535        // sample SPG cannot honour yet — honest error rather than
20536        // a silently ignored seed.
20537        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
20538            self.advance();
20539            let method = self.expect_ident_like()?;
20540            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
20541                return Err(self.err(alloc::format!(
20542                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
20543                )));
20544            }
20545            if !matches!(self.peek(), Token::LParen) {
20546                return Err(self.err(alloc::format!(
20547                    "expected '(' after TABLESAMPLE {}, got {:?}",
20548                    method.to_ascii_uppercase(),
20549                    self.peek()
20550                )));
20551            }
20552            self.advance();
20553            let percent = self.parse_expr(0)?;
20554            if !matches!(self.peek(), Token::RParen) {
20555                return Err(self.err(alloc::format!(
20556                    "expected ')' after TABLESAMPLE percentage, got {:?}",
20557                    self.peek()
20558                )));
20559            }
20560            self.advance();
20561            // REPEATABLE(seed) → a deterministic per-row draw seeded by
20562            // `seed`, so the sample is stable across repeats and rescans.
20563            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
20564            let mut sample_seed: Option<Expr> = None;
20565            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
20566                self.advance();
20567                if !matches!(self.peek(), Token::LParen) {
20568                    return Err(self.err(alloc::format!(
20569                        "expected '(' after REPEATABLE, got {:?}",
20570                        self.peek()
20571                    )));
20572                }
20573                self.advance();
20574                let seed = self.parse_expr(0)?;
20575                if !matches!(self.peek(), Token::RParen) {
20576                    return Err(self.err(alloc::format!(
20577                        "expected ')' after REPEATABLE seed, got {:?}",
20578                        self.peek()
20579                    )));
20580                }
20581                self.advance();
20582                sample_seed = Some(seed);
20583            }
20584            let draw = match sample_seed {
20585                Some(seed) => Expr::FunctionCall {
20586                    name: "__tsm_fract".to_string(),
20587                    args: alloc::vec![seed],
20588                },
20589                None => Expr::FunctionCall {
20590                    name: "random".to_string(),
20591                    args: Vec::new(),
20592                },
20593            };
20594            self.pending_sample_preds.push(Expr::Binary {
20595                lhs: Box::new(draw),
20596                op: crate::ast::BinOp::Lt,
20597                rhs: Box::new(Expr::Binary {
20598                    lhs: Box::new(percent),
20599                    op: crate::ast::BinOp::Div,
20600                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
20601                }),
20602            });
20603        }
20604        Ok(TableRef {
20605            name,
20606            alias,
20607            only,
20608            as_of_segment,
20609            unnest_expr: None,
20610            unnest_column_aliases: Vec::new(),
20611            with_ordinality: false,
20612            generate_series_args: None,
20613            lateral_subquery: None,
20614            jsonb_each_text_arg: None,
20615            table_fn_call: None,
20616            rows_from: None,
20617            json_table: None,
20618            scalar_fn_item: false,
20619        })
20620    }
20621
20622    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
20623    /// but also accepts `AS alias(col [, col, …])` — the
20624    /// PG-standard table-function column-list form. The column
20625    /// list is only honoured when paired with `UNNEST(...)` in
20626    /// the parent; other call sites currently discard it.
20627    /// True when the expression tree contains a qualified column
20628    /// reference (`t.col`) — the syntactic marker that an SRF
20629    /// argument correlates with a preceding FROM item.
20630    fn expr_has_qualified_column(e: &Expr) -> bool {
20631        match e {
20632            Expr::Column(c) => c.qualifier.is_some(),
20633            Expr::Binary { lhs, rhs, .. } => {
20634                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
20635            }
20636            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
20637            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
20638            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
20639            Expr::Case {
20640                operand,
20641                branches,
20642                else_branch,
20643            } => {
20644                operand
20645                    .as_deref()
20646                    .is_some_and(Self::expr_has_qualified_column)
20647                    || branches.iter().any(|(w, t)| {
20648                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
20649                    })
20650                    || else_branch
20651                        .as_deref()
20652                        .is_some_and(Self::expr_has_qualified_column)
20653            }
20654            _ => false,
20655        }
20656    }
20657
20658    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
20659    /// counts a bare (unqualified) column. A set-returning function has no
20660    /// input columns of its own, so ANY column in its arguments is an outer
20661    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
20662    fn expr_has_any_column(e: &Expr) -> bool {
20663        match e {
20664            Expr::Column(_) => true,
20665            Expr::Binary { lhs, rhs, .. } => {
20666                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
20667            }
20668            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
20669            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
20670            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
20671            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
20672            // constructor or subscript fell to the `_ => false` arm, so
20673            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
20674            // channel and the eager peer eval answered `column "x" does
20675            // not exist` (the substitution walker already recurses both
20676            // shapes; only this detector was blind to them).
20677            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
20678            Expr::ArraySubscript { target, index } => {
20679                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
20680            }
20681            Expr::Case {
20682                operand,
20683                branches,
20684                else_branch,
20685            } => {
20686                operand.as_deref().is_some_and(Self::expr_has_any_column)
20687                    || branches
20688                        .iter()
20689                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
20690                    || else_branch
20691                        .as_deref()
20692                        .is_some_and(Self::expr_has_any_column)
20693            }
20694            _ => false,
20695        }
20696    }
20697
20698    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
20699    /// `generate_series(1, t.n)`) into the lateral_subquery
20700    /// channel: `SELECT * FROM <srf>` executes per outer row with
20701    /// outer references substituted (v7.37.43-T4.5 machinery).
20702    /// Uncorrelated SRFs stay on their plain channels.
20703    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
20704        let name = srf.name.clone();
20705        let alias = srf.alias.clone();
20706        let inner = crate::ast::SelectStatement {
20707            locking: None,
20708            ctes: Vec::new(),
20709            distinct: false,
20710            distinct_on: Vec::new(),
20711            items: alloc::vec![crate::ast::SelectItem::Wildcard],
20712            from: Some(crate::ast::FromClause {
20713                primary: srf,
20714                joins: Vec::new(),
20715            }),
20716            where_: None,
20717            group_by: None,
20718            group_by_all: false,
20719            having: None,
20720            unions: Vec::new(),
20721            order_by: Vec::new(),
20722            limit: None,
20723            offset: None,
20724            limit_with_ties: false,
20725            window_check_exprs: Vec::new(),
20726        };
20727        TableRef {
20728            name,
20729            alias,
20730            only: false,
20731            as_of_segment: None,
20732            unnest_expr: None,
20733            unnest_column_aliases: Vec::new(),
20734            with_ordinality: false,
20735            generate_series_args: None,
20736            lateral_subquery: Some(Box::new(inner)),
20737            jsonb_each_text_arg: None,
20738            table_fn_call: None,
20739            rows_from: None,
20740            json_table: None,
20741            scalar_fn_item: false,
20742        }
20743    }
20744
20745    /// True when the expression tree contains an unresolved
20746    /// `OVER w` marker (see parse_over_clause).
20747    fn expr_has_named_window(e: &Expr) -> bool {
20748        match e {
20749            Expr::WindowFunction { partition_by, .. } => matches!(
20750                partition_by.as_slice(),
20751                [Expr::Column(c)] if matches!(
20752                    c.qualifier.as_deref(),
20753                    Some("__named_window__") | Some("__named_window_ref__")
20754                )
20755            ),
20756            Expr::Binary { lhs, rhs, .. } => {
20757                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
20758            }
20759            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
20760            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
20761            Expr::Case {
20762                operand,
20763                branches,
20764                else_branch,
20765            } => {
20766                operand.as_deref().is_some_and(Self::expr_has_named_window)
20767                    || branches.iter().any(|(w, t)| {
20768                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
20769                    })
20770                    || else_branch
20771                        .as_deref()
20772                        .is_some_and(Self::expr_has_named_window)
20773            }
20774            _ => false,
20775        }
20776    }
20777
20778    /// v7.39 (round 705) — the NAMES the expression references through the
20779    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
20780    /// definitions nothing referenced. Traversal mirrors
20781    /// `expr_has_named_window` above.
20782    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
20783        match e {
20784            Expr::WindowFunction { partition_by, .. } => {
20785                if let [Expr::Column(c)] = partition_by.as_slice()
20786                    && matches!(
20787                        c.qualifier.as_deref(),
20788                        Some("__named_window__") | Some("__named_window_ref__")
20789                    )
20790                {
20791                    into.push(c.name.clone());
20792                }
20793            }
20794            Expr::Binary { lhs, rhs, .. } => {
20795                Self::collect_named_window_refs(lhs, into);
20796                Self::collect_named_window_refs(rhs, into);
20797            }
20798            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20799                Self::collect_named_window_refs(expr, into);
20800            }
20801            Expr::FunctionCall { args, .. } => {
20802                for a in args {
20803                    Self::collect_named_window_refs(a, into);
20804                }
20805            }
20806            Expr::Case {
20807                operand,
20808                branches,
20809                else_branch,
20810            } => {
20811                if let Some(o) = operand.as_deref() {
20812                    Self::collect_named_window_refs(o, into);
20813                }
20814                for (w, t) in branches {
20815                    Self::collect_named_window_refs(w, into);
20816                    Self::collect_named_window_refs(t, into);
20817                }
20818                if let Some(eb) = else_branch.as_deref() {
20819                    Self::collect_named_window_refs(eb, into);
20820                }
20821            }
20822            _ => {}
20823        }
20824    }
20825
20826    /// Inline named-window definitions into the `OVER w` markers.
20827    /// An unknown name errors (PG: window "w" does not exist).
20828    #[allow(clippy::type_complexity)]
20829    fn substitute_named_windows(
20830        e: &mut Expr,
20831        defs: &[(
20832            String,
20833            (
20834                Vec<Expr>,
20835                Vec<(Expr, bool, Option<bool>)>,
20836                Option<WindowFrame>,
20837            ),
20838        )],
20839    ) -> Result<(), String> {
20840        match e {
20841            Expr::WindowFunction {
20842                partition_by,
20843                order_by,
20844                frame,
20845                ..
20846            } => {
20847                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20848                // from the bare `OVER w1` (a plain reference).
20849                let named = match partition_by.as_slice() {
20850                    [Expr::Column(c)] => match c.qualifier.as_deref() {
20851                        Some("__named_window__") => Some((c.name.clone(), false)),
20852                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
20853                        _ => None,
20854                    },
20855                    _ => None,
20856                };
20857                if let Some((wname, is_copy)) = named {
20858                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20859                    else {
20860                        return Err(alloc::format!("window {wname:?} does not exist"));
20861                    };
20862                    if !is_copy {
20863                        *partition_by = def.0.clone();
20864                        *order_by = def.1.clone();
20865                        *frame = def.2.clone();
20866                        return Ok(());
20867                    }
20868                    // v7.39 (round 229) — PG's copy rules, probed against
20869                    // 18.4: a copy inherits the partitioning, may supply an
20870                    // ordering only when the base has none, and may not copy
20871                    // a base that already carries a frame (its own frame
20872                    // would be ambiguous with the inherited one).
20873                    if !def.1.is_empty() && !order_by.is_empty() {
20874                        return Err(alloc::format!(
20875                            "cannot override ORDER BY clause of window \"{wname}\""
20876                        ));
20877                    }
20878                    if def.2.is_some() {
20879                        return Err(alloc::format!(
20880                            "cannot copy window \"{wname}\" because it has a frame clause"
20881                        ));
20882                    }
20883                    *partition_by = def.0.clone();
20884                    if order_by.is_empty() {
20885                        *order_by = def.1.clone();
20886                    }
20887                }
20888                Ok(())
20889            }
20890            Expr::Binary { lhs, rhs, .. } => {
20891                Self::substitute_named_windows(lhs, defs)?;
20892                Self::substitute_named_windows(rhs, defs)
20893            }
20894            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20895                Self::substitute_named_windows(expr, defs)
20896            }
20897            Expr::FunctionCall { args, .. } => {
20898                for a in args {
20899                    Self::substitute_named_windows(a, defs)?;
20900                }
20901                Ok(())
20902            }
20903            Expr::Case {
20904                operand,
20905                branches,
20906                else_branch,
20907            } => {
20908                if let Some(op) = operand {
20909                    Self::substitute_named_windows(op, defs)?;
20910                }
20911                for (w, t) in branches {
20912                    Self::substitute_named_windows(w, defs)?;
20913                    Self::substitute_named_windows(t, defs)?;
20914                }
20915                if let Some(el) = else_branch {
20916                    Self::substitute_named_windows(el, defs)?;
20917                }
20918                Ok(())
20919            }
20920            _ => Ok(()),
20921        }
20922    }
20923
20924    /// SQL-standard `TABLE name` shorthand — builds the equivalent
20925    /// `SELECT * FROM name` head. Callers own set-op chain / tail
20926    /// composition.
20927    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20928        debug_assert!(matches!(self.peek(), Token::Table));
20929        self.advance(); // TABLE
20930        let tname = self.expect_ident_like()?;
20931        Ok(SelectStatement {
20932            locking: None,
20933            ctes: Vec::new(),
20934            distinct: false,
20935            distinct_on: Vec::new(),
20936            items: alloc::vec![SelectItem::Wildcard],
20937            from: Some(FromClause {
20938                primary: TableRef {
20939                    name: tname,
20940                    alias: None,
20941                    only: false,
20942                    as_of_segment: None,
20943                    unnest_expr: None,
20944                    unnest_column_aliases: Vec::new(),
20945                    with_ordinality: false,
20946                    generate_series_args: None,
20947                    lateral_subquery: None,
20948                    jsonb_each_text_arg: None,
20949                    table_fn_call: None,
20950                    rows_from: None,
20951                    json_table: None,
20952                    scalar_fn_item: false,
20953                },
20954                joins: Vec::new(),
20955            }),
20956            where_: None,
20957            group_by: None,
20958            group_by_all: false,
20959            having: None,
20960            unions: Vec::new(),
20961            order_by: Vec::new(),
20962            limit: None,
20963            offset: None,
20964            limit_with_ties: false,
20965            window_check_exprs: Vec::new(),
20966        })
20967    }
20968
20969    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20970    /// variants) → a derived table that reads each declared column out of
20971    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20972    /// `jsonb_array_elements(J)` (one row per element, column `value`);
20973    /// the scalar *record form projects a single row straight off `J`.
20974    /// Rides the existing lateral-subquery channel, so no new executor or
20975    /// AST is needed.
20976    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20977        use crate::ast::{
20978            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20979        };
20980        let fn_name = match self.peek() {
20981            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20982            _ => unreachable!("caller guarded is_json_to_record_name"),
20983        };
20984        self.advance(); // fn name
20985        self.advance(); // (
20986        let mut arg = self.parse_expr(0)?;
20987        // populate_record(base, json): the base only carries the record
20988        // type here — the JSON argument is the second expression.
20989        let mut base: Option<Expr> = None;
20990        if matches!(self.peek(), Token::Comma) {
20991            self.advance();
20992            base = Some(arg);
20993            arg = self.parse_expr(0)?;
20994        }
20995        if !matches!(self.peek(), Token::RParen) {
20996            return Err(self.err(alloc::format!(
20997                "expected ')' after {fn_name}() argument, got {:?}",
20998                self.peek()
20999            )));
21000        }
21001        self.advance(); // )
21002        let is_set = fn_name.ends_with("recordset");
21003        // `[AS] alias ( col type [, …] )` column-definition list.
21004        if matches!(self.peek(), Token::As) {
21005            self.advance();
21006        }
21007        let alias_opt = match self.peek() {
21008            Token::Ident(s) | Token::QuotedIdent(s) => {
21009                let a = s.clone();
21010                self.advance();
21011                Some(a)
21012            }
21013            _ => None,
21014        };
21015        // v7.39 (read01 round 76) — the populate family's canonical PG
21016        // spelling carries no column list at all: the row shape comes from
21017        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
21018        // j)`). The parser has no catalog, so hand the two arguments to the
21019        // engine's table-function channel, which does. Only `*_to_record*`
21020        // (whose base is bare `record`) genuinely requires the list.
21021        if !matches!(self.peek(), Token::LParen) {
21022            if let Some(base_expr) = base {
21023                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
21024                return Ok(TableRef {
21025                    name: alias.clone(),
21026                    alias: Some(alias),
21027                    only: false,
21028                    as_of_segment: None,
21029                    unnest_expr: None,
21030                    unnest_column_aliases: Vec::new(),
21031                    with_ordinality: false,
21032                    generate_series_args: None,
21033                    lateral_subquery: None,
21034                    jsonb_each_text_arg: None,
21035                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
21036                    rows_from: None,
21037                    json_table: None,
21038                    scalar_fn_item: false,
21039                });
21040            }
21041            return Err(self.err(alloc::format!(
21042                "expected '(' to start the {fn_name} column-definition list, got {:?}",
21043                self.peek()
21044            )));
21045        }
21046        let Some(alias) = alias_opt else {
21047            return Err(self.err(alloc::format!(
21048                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
21049            )));
21050        };
21051        self.advance(); // (
21052        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
21053        loop {
21054            let col = self.expect_ident_like()?;
21055            let ty = self.parse_cast_target()?;
21056            coldefs.push((col, ty));
21057            if matches!(self.peek(), Token::Comma) {
21058                self.advance();
21059                continue;
21060            }
21061            if matches!(self.peek(), Token::RParen) {
21062                self.advance();
21063                break;
21064            }
21065            return Err(self.err(alloc::format!(
21066                "expected ',' or ')' in {fn_name} column list, got {:?}",
21067                self.peek()
21068            )));
21069        }
21070        if coldefs.is_empty() {
21071            return Err(self.err(alloc::format!(
21072                "{fn_name} column-definition list must declare at least one column"
21073            )));
21074        }
21075        // Per column: (base ->> 'col')::type AS col. The base is the
21076        // per-element `value` column for the *set form, or the argument
21077        // itself for the scalar record form.
21078        let items: Vec<SelectItem> = coldefs
21079            .into_iter()
21080            .map(|(col, ty)| {
21081                let base = if is_set {
21082                    Expr::Column(ColumnName {
21083                        qualifier: None,
21084                        name: "value".to_string(),
21085                    })
21086                } else {
21087                    arg.clone()
21088                };
21089                SelectItem::Expr {
21090                    expr: Expr::Cast {
21091                        expr: Box::new(Expr::Binary {
21092                            lhs: Box::new(base),
21093                            op: BinOp::JsonGetText,
21094                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
21095                        }),
21096                        target: ty,
21097                    },
21098                    alias: Some(col),
21099                }
21100            })
21101            .collect();
21102        let from = if is_set {
21103            let elem_fn = if fn_name.starts_with("jsonb") {
21104                "jsonb_array_elements"
21105            } else {
21106                "json_array_elements"
21107            };
21108            Some(FromClause {
21109                primary: TableRef {
21110                    name: "value".to_string(),
21111                    alias: None,
21112                    only: false,
21113                    as_of_segment: None,
21114                    unnest_expr: Some(Box::new(Expr::FunctionCall {
21115                        name: elem_fn.to_string(),
21116                        args: alloc::vec![arg],
21117                    })),
21118                    unnest_column_aliases: alloc::vec!["value".to_string()],
21119                    with_ordinality: false,
21120                    generate_series_args: None,
21121                    lateral_subquery: None,
21122                    jsonb_each_text_arg: None,
21123                    table_fn_call: None,
21124                    rows_from: None,
21125                    json_table: None,
21126                    scalar_fn_item: false,
21127                },
21128                joins: Vec::new(),
21129            })
21130        } else {
21131            None
21132        };
21133        let inner = SelectStatement {
21134            locking: None,
21135            ctes: Vec::new(),
21136            distinct: false,
21137            distinct_on: Vec::new(),
21138            items,
21139            from,
21140            where_: None,
21141            group_by: None,
21142            group_by_all: false,
21143            having: None,
21144            unions: Vec::new(),
21145            order_by: Vec::new(),
21146            limit: None,
21147            offset: None,
21148            limit_with_ties: false,
21149            window_check_exprs: Vec::new(),
21150        };
21151        Ok(TableRef {
21152            name: alias.clone(),
21153            alias: Some(alias),
21154            only: false,
21155            as_of_segment: None,
21156            unnest_expr: None,
21157            unnest_column_aliases: Vec::new(),
21158            with_ordinality: false,
21159            generate_series_args: None,
21160            lateral_subquery: Some(Box::new(inner)),
21161            jsonb_each_text_arg: None,
21162            table_fn_call: None,
21163            rows_from: None,
21164            json_table: None,
21165            scalar_fn_item: false,
21166        })
21167    }
21168
21169    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
21170    /// Returns true when the clause was present. `WITH` alone (a
21171    /// CTE can never start here) is not enough — the ORDINALITY
21172    /// ident must follow, so a stray WITH still errors downstream.
21173    fn absorb_with_ordinality(&mut self) -> bool {
21174        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
21175            && matches!(self.tokens.get(self.pos + 1),
21176                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
21177        {
21178            self.advance();
21179            self.advance();
21180            true
21181        } else {
21182            false
21183        }
21184    }
21185
21186    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
21187    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
21188    /// Out-of-line: the caller sits on the FROM recursion chain.
21189    #[inline(never)]
21190    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
21191        let fn_name = match self.advance() {
21192            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
21193            _ => unreachable!("caller peeked an ident"),
21194        };
21195        self.advance(); // (
21196        let mut args: Vec<Expr> = Vec::new();
21197        if !matches!(self.peek(), Token::RParen) {
21198            loop {
21199                args.push(self.parse_expr(0)?);
21200                if matches!(self.peek(), Token::Comma) {
21201                    self.advance();
21202                    continue;
21203                }
21204                break;
21205            }
21206        }
21207        if !matches!(self.peek(), Token::RParen) {
21208            return Err(self.err(alloc::format!(
21209                "expected ')' after {fn_name}() arguments, got {:?}",
21210                self.peek()
21211            )));
21212        }
21213        self.advance();
21214        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
21215        // counter column rides after the function's own, and the alias list
21216        // names it.
21217        let with_ordinality = self.absorb_with_ordinality();
21218        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
21219        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
21220        Ok(TableRef {
21221            name,
21222            alias: alias_ident,
21223            only: false,
21224            as_of_segment: None,
21225            unnest_expr: None,
21226            unnest_column_aliases,
21227            with_ordinality,
21228            generate_series_args: None,
21229            lateral_subquery: None,
21230            jsonb_each_text_arg: None,
21231            table_fn_call: Some(Box::new((fn_name, args))),
21232            rows_from: None,
21233            json_table: None,
21234            scalar_fn_item: false,
21235        })
21236    }
21237
21238    /// v7.39 (round 205, JSON_TABLE) — parse
21239    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
21240    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
21241    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
21242    #[inline(never)]
21243    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
21244        self.advance(); // json_table
21245        self.advance(); // (
21246        let doc = Box::new(self.parse_expr(0)?);
21247        self.expect_comma_json_table()?;
21248        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
21249        // Optional `PASSING <expr> AS <name> [, …]`.
21250        let mut passing: Vec<(String, Expr)> = Vec::new();
21251        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
21252            self.advance();
21253            loop {
21254                let e = self.parse_expr(0)?;
21255                if !matches!(self.peek(), Token::As) {
21256                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
21257                }
21258                self.advance();
21259                let vname = match self.advance() {
21260                    Token::Ident(s) | Token::QuotedIdent(s) => s,
21261                    other => {
21262                        return Err(self.err(alloc::format!(
21263                            "expected PASSING variable name, got {other:?}"
21264                        )));
21265                    }
21266                };
21267                passing.push((vname, e));
21268                if matches!(self.peek(), Token::Comma) {
21269                    self.advance();
21270                    continue;
21271                }
21272                break;
21273            }
21274        }
21275        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
21276            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
21277        }
21278        self.advance();
21279        let columns = self.parse_json_table_columns()?;
21280        if !matches!(self.peek(), Token::RParen) {
21281            return Err(self.err(alloc::format!(
21282                "expected ')' to close JSON_TABLE, got {:?}",
21283                self.peek()
21284            )));
21285        }
21286        self.advance();
21287        let alias_ident = self.parse_optional_alias()?;
21288        let name = alias_ident
21289            .clone()
21290            .unwrap_or_else(|| String::from("json_table"));
21291        Ok(TableRef {
21292            name,
21293            alias: alias_ident,
21294            only: false,
21295            as_of_segment: None,
21296            unnest_expr: None,
21297            unnest_column_aliases: Vec::new(),
21298            with_ordinality: false,
21299            generate_series_args: None,
21300            lateral_subquery: None,
21301            jsonb_each_text_arg: None,
21302            table_fn_call: None,
21303            rows_from: None,
21304            json_table: Some(Box::new(crate::ast::JsonTable {
21305                doc,
21306                row_path,
21307                columns,
21308                passing,
21309            })),
21310            scalar_fn_item: false,
21311        })
21312    }
21313
21314    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
21315        if !matches!(self.peek(), Token::Comma) {
21316            return Err(self.err(alloc::format!(
21317                "expected ',' after JSON_TABLE document, got {:?}",
21318                self.peek()
21319            )));
21320        }
21321        self.advance();
21322        Ok(())
21323    }
21324
21325    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
21326        match self.advance() {
21327            Token::String(s) => Ok(s),
21328            other => Err(self.err(alloc::format!(
21329                "expected {what} string literal, got {other:?}"
21330            ))),
21331        }
21332    }
21333
21334    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
21335    #[inline(never)]
21336    fn parse_json_table_columns(
21337        &mut self,
21338    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
21339        if !matches!(self.peek(), Token::LParen) {
21340            return Err(self.err("expected '(' after COLUMNS".into()));
21341        }
21342        self.advance();
21343        let mut cols = Vec::new();
21344        loop {
21345            cols.push(self.parse_json_table_one_column()?);
21346            if matches!(self.peek(), Token::Comma) {
21347                self.advance();
21348                continue;
21349            }
21350            break;
21351        }
21352        if !matches!(self.peek(), Token::RParen) {
21353            return Err(self.err(alloc::format!(
21354                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
21355                self.peek()
21356            )));
21357        }
21358        self.advance();
21359        Ok(cols)
21360    }
21361
21362    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
21363        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
21364        // NESTED [PATH] '<p>' COLUMNS (...)
21365        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
21366            self.advance();
21367            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
21368                self.advance();
21369            }
21370            let path = self.parse_json_string_literal("NESTED PATH")?;
21371            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
21372                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
21373            }
21374            self.advance();
21375            let columns = self.parse_json_table_columns()?;
21376            return Ok(JsonTableColumn::Nested { path, columns });
21377        }
21378        // <name> ...
21379        let name = match self.advance() {
21380            Token::Ident(s) | Token::QuotedIdent(s) => s,
21381            other => {
21382                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
21383            }
21384        };
21385        // <name> FOR ORDINALITY
21386        if matches!(self.peek(), Token::For) {
21387            self.advance();
21388            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
21389                return Err(self.err("expected ORDINALITY after FOR".into()));
21390            }
21391            self.advance();
21392            return Ok(JsonTableColumn::Ordinality { name });
21393        }
21394        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
21395        let ty = self.parse_column_type_name()?;
21396        let mut format_json = false;
21397        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
21398            self.advance();
21399            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
21400                return Err(self.err("expected JSON after FORMAT".into()));
21401            }
21402            self.advance();
21403            format_json = true;
21404        }
21405        let mut exists = false;
21406        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
21407            self.advance();
21408            exists = true;
21409        }
21410        let mut path = alloc::format!("$.{name}");
21411        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
21412            self.advance();
21413            path = self.parse_json_string_literal("column PATH")?;
21414        }
21415        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
21416            // `FORMAT JSON` after PATH (alternate placement).
21417            self.advance();
21418            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
21419                self.advance();
21420            }
21421            format_json = true;
21422        }
21423        let mut wrapper = false;
21424        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
21425            self.advance();
21426            // optional CONDITIONAL/UNCONDITIONAL
21427            if matches!(self.peek(), Token::Ident(s)
21428                if s.eq_ignore_ascii_case("unconditional")
21429                    || s.eq_ignore_ascii_case("conditional"))
21430            {
21431                self.advance();
21432            }
21433            if !matches!(self.peek(), Token::Ident(s)
21434                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
21435            {
21436                return Err(self.err("expected WRAPPER after WITH".into()));
21437            }
21438            self.advance();
21439            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
21440            if matches!(self.peek(), Token::Ident(s)
21441                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
21442            {
21443                self.advance();
21444            }
21445            wrapper = true;
21446        }
21447        // ON EMPTY / ON ERROR clauses (two, in any order).
21448        let mut on_empty = JsonTableOnBehavior::Null;
21449        let mut on_error = JsonTableOnBehavior::Null;
21450        for _ in 0..2 {
21451            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
21452            {
21453                self.advance();
21454                Some(JsonTableOnBehavior::Error)
21455            } else if matches!(self.peek(), Token::Null) {
21456                self.advance();
21457                Some(JsonTableOnBehavior::Null)
21458            } else if matches!(self.peek(), Token::Default) {
21459                self.advance();
21460                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
21461            } else {
21462                None
21463            };
21464            let Some(behavior) = behavior else { break };
21465            // `ON {EMPTY|ERROR}`
21466            if !matches!(self.peek(), Token::On) {
21467                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
21468            }
21469            self.advance();
21470            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
21471                self.advance();
21472                on_empty = behavior;
21473            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
21474                self.advance();
21475                on_error = behavior;
21476            } else {
21477                return Err(self.err("expected EMPTY or ERROR after ON".into()));
21478            }
21479        }
21480        Ok(JsonTableColumn::Regular {
21481            name,
21482            ty,
21483            path,
21484            exists,
21485            format_json,
21486            wrapper,
21487            on_empty,
21488            on_error,
21489        })
21490    }
21491
21492    fn parse_optional_alias_with_columns(
21493        &mut self,
21494    ) -> Result<(Option<String>, Vec<String>), ParseError> {
21495        let alias = self.parse_optional_alias()?;
21496        if alias.is_none() {
21497            return Ok((None, Vec::new()));
21498        }
21499        let mut cols: Vec<String> = Vec::new();
21500        if matches!(self.peek(), Token::LParen) {
21501            self.advance();
21502            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
21503                self.advance();
21504                cols.push(s);
21505                if matches!(self.peek(), Token::Comma) {
21506                    self.advance();
21507                    continue;
21508                }
21509                break;
21510            }
21511            if matches!(self.peek(), Token::RParen) {
21512                self.advance();
21513            }
21514        }
21515        Ok((alias, cols))
21516    }
21517
21518    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
21519    /// whose keyword token was already consumed and whose `(` is the
21520    /// current token. Factored out of `parse_atom` (and marked
21521    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
21522    /// recursive `parse_atom` frame — inlining them there enlarges the
21523    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
21524    /// against, risking an overflow before the budget triggers.
21525    #[inline(never)]
21526    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21527        self.advance(); // (
21528        let mut args = Vec::new();
21529        if !matches!(self.peek(), Token::RParen) {
21530            loop {
21531                args.push(self.parse_expr(0)?);
21532                match self.peek() {
21533                    Token::Comma => {
21534                        self.advance();
21535                    }
21536                    Token::RParen => break,
21537                    other => {
21538                        return Err(self.err(alloc::format!(
21539                            "expected ',' or ')' in {name}() args, got {other:?}"
21540                        )));
21541                    }
21542                }
21543            }
21544        }
21545        self.advance(); // )
21546        Ok(Expr::FunctionCall {
21547            name: name.into(),
21548            args,
21549        })
21550    }
21551
21552    /// FROM-clause: a primary table reference plus zero-or-more joined
21553    /// peers expressed via either `, <table>` (cross-product, no ON) or
21554    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
21555    /// v1.10 keeps the join list flat (left-associative nested-loop
21556    /// semantics).
21557    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
21558        let primary = self.parse_table_ref()?;
21559        let primary_qual = primary
21560            .alias
21561            .clone()
21562            .unwrap_or_else(|| primary.name.clone());
21563        let joins = self.parse_from_joins(&primary_qual)?;
21564        Ok(FromClause { primary, joins })
21565    }
21566
21567    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
21568    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
21569    /// SAME grammar after its target table has already been consumed.
21570    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
21571    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
21572    /// be parsed forward, once.)
21573    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
21574    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
21575    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
21576    /// desugaring, which needs a name for the left side of each equality.
21577    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
21578        let mut joins = Vec::new();
21579        loop {
21580            // `, <table>` — cross-product with no ON.
21581            if matches!(self.peek(), Token::Comma) {
21582                self.advance();
21583                let table = self.parse_table_ref()?;
21584                joins.push(FromJoin {
21585                    kind: JoinKind::Cross,
21586                    table,
21587                    on: None,
21588                    using_cols: None,
21589                    natural: false,
21590                });
21591                continue;
21592            }
21593            // v7.37.16 — optional leading `NATURAL` before the join
21594            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
21595            // not a lexer keyword (it arrives as a bare Ident), so match
21596            // it case-insensitively here. When present, no ON/USING
21597            // clause is allowed — the common columns are resolved at
21598            // execution time.
21599            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
21600            if natural {
21601                self.advance();
21602            }
21603            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
21604            // CROSS JOIN, and bare JOIN (defaults to INNER).
21605            let kind =
21606                match self.peek() {
21607                    Token::Inner => {
21608                        self.advance();
21609                        if !matches!(self.peek(), Token::Join) {
21610                            return Err(self
21611                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
21612                        }
21613                        self.advance();
21614                        JoinKind::Inner
21615                    }
21616                    Token::Left => {
21617                        self.advance();
21618                        if matches!(self.peek(), Token::Outer) {
21619                            self.advance();
21620                        }
21621                        if !matches!(self.peek(), Token::Join) {
21622                            return Err(self.err(format!(
21623                                "expected JOIN after LEFT [OUTER], got {:?}",
21624                                self.peek()
21625                            )));
21626                        }
21627                        self.advance();
21628                        JoinKind::Left
21629                    }
21630                    Token::Cross => {
21631                        self.advance();
21632                        if !matches!(self.peek(), Token::Join) {
21633                            return Err(self
21634                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
21635                        }
21636                        self.advance();
21637                        JoinKind::Cross
21638                    }
21639                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
21640                    Token::Right => {
21641                        self.advance();
21642                        if matches!(self.peek(), Token::Outer) {
21643                            self.advance();
21644                        }
21645                        if !matches!(self.peek(), Token::Join) {
21646                            return Err(self.err(format!(
21647                                "expected JOIN after RIGHT [OUTER], got {:?}",
21648                                self.peek()
21649                            )));
21650                        }
21651                        self.advance();
21652                        JoinKind::Right
21653                    }
21654                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
21655                    Token::Full => {
21656                        self.advance();
21657                        if matches!(self.peek(), Token::Outer) {
21658                            self.advance();
21659                        }
21660                        if !matches!(self.peek(), Token::Join) {
21661                            return Err(self.err(format!(
21662                                "expected JOIN after FULL [OUTER], got {:?}",
21663                                self.peek()
21664                            )));
21665                        }
21666                        self.advance();
21667                        JoinKind::FullOuter
21668                    }
21669                    Token::Join => {
21670                        self.advance();
21671                        JoinKind::Inner
21672                    }
21673                    _ => break,
21674                };
21675            let table = self.parse_table_ref()?;
21676            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
21677            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
21678            // where prev_table is the most-recent left-side table
21679            // (the previous join's table if any, else the FROM primary).
21680            // PG semantics around column merging are richer (USING'd
21681            // cols become deduplicated single output columns); for
21682            // sugar purposes the predicate-only form covers the
21683            // baseline corpus shape and chained `… JOIN x USING (k)
21684            // JOIN y USING (k)` calls.
21685            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
21686            // common columns resolve at execution time.
21687            if natural {
21688                joins.push(FromJoin {
21689                    kind,
21690                    table,
21691                    on: None,
21692                    using_cols: None,
21693                    natural: true,
21694                });
21695                continue;
21696            }
21697            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
21698            // v7.37.16 — capture the USING column list (in addition to
21699            // the ON desugar below) so the executor can perform PG's
21700            // column-merge on the output side.
21701            let mut using_cols: Option<Vec<String>> = None;
21702            let on = if matches!(self.peek(), Token::On) {
21703                self.advance();
21704                Some(self.parse_expr(0)?)
21705            } else if using_match {
21706                self.advance();
21707                if !matches!(self.peek(), Token::LParen) {
21708                    return Err(
21709                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
21710                    );
21711                }
21712                self.advance();
21713                let mut cols: Vec<String> = Vec::new();
21714                loop {
21715                    match self.peek().clone() {
21716                        Token::Ident(s) | Token::QuotedIdent(s) => {
21717                            self.advance();
21718                            cols.push(s);
21719                        }
21720                        other => {
21721                            return Err(self.err(format!(
21722                                "expected column name inside USING (…), got {other:?}"
21723                            )));
21724                        }
21725                    }
21726                    match self.peek() {
21727                        Token::Comma => {
21728                            self.advance();
21729                            continue;
21730                        }
21731                        Token::RParen => {
21732                            self.advance();
21733                            break;
21734                        }
21735                        other => {
21736                            return Err(self.err(format!(
21737                                "expected ',' or ')' inside USING (…), got {other:?}"
21738                            )));
21739                        }
21740                    }
21741                }
21742                if cols.is_empty() {
21743                    return Err(self.err("USING (…) requires at least one column".to_string()));
21744                }
21745                using_cols = Some(cols.clone());
21746                // Pick the left-side alias: prev join's table if any,
21747                // else FROM primary. Use alias when present, else
21748                // table name (PG-equivalent qualifier).
21749                let left_qual: String = joins
21750                    .last()
21751                    .map(|j| {
21752                        j.table
21753                            .alias
21754                            .clone()
21755                            .unwrap_or_else(|| j.table.name.clone())
21756                    })
21757                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
21758                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
21759                let mut iter = cols.into_iter().map(|c| Expr::Binary {
21760                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21761                        qualifier: Some(left_qual.clone()),
21762                        name: c.clone(),
21763                    })),
21764                    op: crate::ast::BinOp::Eq,
21765                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21766                        qualifier: Some(right_qual.clone()),
21767                        name: c,
21768                    })),
21769                });
21770                let first = iter.next().expect("at least one col");
21771                Some(iter.fold(first, |acc, pred| Expr::Binary {
21772                    lhs: alloc::boxed::Box::new(acc),
21773                    op: crate::ast::BinOp::And,
21774                    rhs: alloc::boxed::Box::new(pred),
21775                }))
21776            } else if kind == JoinKind::Cross {
21777                None
21778            } else {
21779                return Err(self.err(format!(
21780                    "expected ON or USING after {:?} JOIN, got {:?}",
21781                    kind,
21782                    self.peek()
21783                )));
21784            };
21785            joins.push(FromJoin {
21786                kind,
21787                table,
21788                on,
21789                using_cols,
21790                natural: false,
21791            });
21792        }
21793        Ok(joins)
21794    }
21795
21796    /// Optional alias after an expression or table:
21797    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
21798    /// accepted (PG-style implicit alias). Returns `None` if the next token
21799    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
21800    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
21801        if matches!(self.peek(), Token::As) {
21802            self.advance();
21803            // v7.39 (round 340, V56) — after AS the next token MUST be an
21804            // identifier. This used to return None and "let the caller
21805            // surface the error on the next expectation", but when AS is
21806            // the LAST token there is no next expectation: `SELECT 1 AS`
21807            // parsed clean and silently dropped the alias. PG rejects it.
21808            // v7.40.11 — a keyword is a legal alias after AS.
21809            //
21810            // `expect_ident_like` has known the unreserved class since
21811            // v7.17, and this guard never let a keyword token reach it.
21812            // So every one of them was a syntax error in alias position
21813            // while being accepted as a column name in the same build:
21814            //
21815            //   CREATE TABLE rk (release int)   accepted
21816            //   SELECT release FROM rk          accepted
21817            //   SELECT 1 AS release             syntax error
21818            //
21819            // Reported against 7.40.9 for `release` and `savepoint` —
21820            // two statements in a shipped subcommand of the reporter's
21821            // could not be parsed — and it is the whole class, `show`
21822            // and `index` included.
21823            //
21824            // Measured on PG 18.6: after AS, EVERY keyword is a legal
21825            // label, `limit` and `between` included. This accepts the
21826            // ones this parser can name, which is the unreserved class;
21827            // a reserved keyword after AS is still refused here and PG
21828            // takes it.
21829            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
21830                return self.expect_ident_like().map(Some);
21831            }
21832            if unreserved_keyword_text(self.peek()).is_some() {
21833                return self.expect_ident_like().map(Some);
21834            }
21835            return Err(self.err(alloc::format!(
21836                "expected an alias after AS, got {:?}",
21837                self.peek()
21838            )));
21839        }
21840        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21841        // grammar reserves a long list of follow-keywords from the
21842        // alias slot. SPG's bareword approximation: skip a small
21843        // set of idents that would otherwise be swallowed as the
21844        // table alias and break trailing clauses like CREATE
21845        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21846        // CONFLICT WHERE shapes.
21847        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21848            if is_alias_stopword(s) {
21849                return Ok(None);
21850            }
21851            return Ok(self.expect_ident_like().ok());
21852        }
21853        // v7.40.11 — and a keyword, WITHOUT `AS`, which PG also takes:
21854        // `SELECT 1 release` answers 1 there.
21855        //
21856        // Not the ones that begin a trailing clause. PG reserves those
21857        // for exactly this reason and so must this: measured on PG 18.6,
21858        // `SELECT 1 limit` is `syntax error at end of input` — it read
21859        // `limit` as the clause, not as a label. Swallowing it here
21860        // would turn `SELECT 1 limit 2` into a two-token nonsense.
21861        if !matches!(self.peek(), Token::Limit | Token::Offset)
21862            && unreserved_keyword_text(self.peek()).is_some()
21863        {
21864            return Ok(self.expect_ident_like().ok());
21865        }
21866        Ok(None)
21867    }
21868
21869    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21870    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21871        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21872        // error beats a stack overflow (an overflow aborts the
21873        // embedding host process).
21874        self.enter_nested()?;
21875        let r = self.parse_expr_inner(min_prec);
21876        self.nest_depth -= 1;
21877        r
21878    }
21879
21880    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21881    /// When the upcoming tokens form one, return the underlying
21882    /// operator token and the position just past the closing paren
21883    /// so the binary loop can dispatch on the plain operator.
21884    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21885        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21886            return None;
21887        }
21888        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21889            return None;
21890        }
21891        let mut i = self.pos + 2;
21892        // Optional schema qualifier (pg_catalog.<op> etc.).
21893        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21894            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21895        {
21896            i += 2;
21897        }
21898        let op_tok = self.tokens.get(i)?.clone();
21899        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21900            return None;
21901        }
21902        Some((i + 2, op_tok))
21903    }
21904
21905    /// PG operator symbols that lower onto function calls in
21906    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21907    /// family → regexp_like, comparison rung), `^@` (starts_with,
21908    /// comparison rung), `^` (power, tighter than `*`), `#`
21909    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21910    /// subset of the OR bits so the subtraction never borrows).
21911    fn try_symbol_operator(
21912        &mut self,
21913        lhs: &Expr,
21914        min_prec: u8,
21915    ) -> Result<Option<Expr>, ParseError> {
21916        enum Sym {
21917            Regex { ci: bool, negated: bool },
21918            Like { ci: bool, negated: bool },
21919            StartsWith,
21920            Power,
21921            Xor,
21922            RangeAdjacent,
21923        }
21924        // v7.39 (IS-precedence knife) — the low-precedence postfix
21925        // predicates ride this existing leaf call (zero new frame slots
21926        // on the nesting chain).
21927        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21928            return Ok(Some(e));
21929        }
21930        let (sym, prec): (Sym, u8) = match self.peek() {
21931            Token::Tilde => (
21932                Sym::Regex {
21933                    ci: false,
21934                    negated: false,
21935                },
21936                5,
21937            ),
21938            Token::TildeStar => (
21939                Sym::Regex {
21940                    ci: true,
21941                    negated: false,
21942                },
21943                5,
21944            ),
21945            Token::NotTilde => (
21946                Sym::Regex {
21947                    ci: false,
21948                    negated: true,
21949                },
21950                5,
21951            ),
21952            Token::NotTildeStar => (
21953                Sym::Regex {
21954                    ci: true,
21955                    negated: true,
21956                },
21957                5,
21958            ),
21959            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21960            Token::DoubleTilde => (
21961                Sym::Like {
21962                    ci: false,
21963                    negated: false,
21964                },
21965                5,
21966            ),
21967            Token::DoubleTildeStar => (
21968                Sym::Like {
21969                    ci: true,
21970                    negated: false,
21971                },
21972                5,
21973            ),
21974            Token::NotDoubleTilde => (
21975                Sym::Like {
21976                    ci: false,
21977                    negated: true,
21978                },
21979                5,
21980            ),
21981            Token::NotDoubleTildeStar => (
21982                Sym::Like {
21983                    ci: true,
21984                    negated: true,
21985                },
21986                5,
21987            ),
21988            Token::CaretAt => (Sym::StartsWith, 5),
21989            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21990            // tighter than `* / & |`, which the prec-9 rung preserves —
21991            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21992            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21993            Token::Caret => (Sym::Power, 9),
21994            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21995            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21996            Token::Hash => (Sym::Xor, 6),
21997            Token::Adjacent => (Sym::RangeAdjacent, 5),
21998            _ => return Ok(None),
21999        };
22000        if prec < min_prec {
22001            return Ok(None);
22002        }
22003        self.advance();
22004        let rhs = self.parse_expr(prec + 1)?;
22005        let out = match sym {
22006            Sym::Regex { ci, negated } => {
22007                let mut args = alloc::vec![lhs.clone(), rhs];
22008                if ci {
22009                    args.push(Expr::Literal(Literal::String(String::from("i"))));
22010                }
22011                maybe_not(
22012                    Expr::FunctionCall {
22013                        name: String::from("regexp_like"),
22014                        args,
22015                    },
22016                    negated,
22017                )
22018            }
22019            Sym::Like { ci, negated } => Expr::Like {
22020                expr: alloc::boxed::Box::new(lhs.clone()),
22021                pattern: alloc::boxed::Box::new(rhs),
22022                negated,
22023                case_insensitive: ci,
22024            },
22025            Sym::StartsWith => Expr::FunctionCall {
22026                name: String::from("starts_with"),
22027                args: alloc::vec![lhs.clone(), rhs],
22028            },
22029            Sym::Power => Expr::FunctionCall {
22030                name: String::from("power"),
22031                args: alloc::vec![lhs.clone(), rhs],
22032            },
22033            // `#` bitwise XOR — a real operator now (was desugared to
22034            // `(a|b)-(a&b)`, algebraically identical for integers but
22035            // undefined for bit strings; the direct op handles both).
22036            Sym::Xor => Expr::Binary {
22037                lhs: Box::new(lhs.clone()),
22038                op: BinOp::BitXor,
22039                rhs: Box::new(rhs),
22040            },
22041            // range `-|-` "is adjacent to" — lowered to a catalog function.
22042            Sym::RangeAdjacent => Expr::FunctionCall {
22043                name: String::from("range_adjacent"),
22044                args: alloc::vec![lhs.clone(), rhs],
22045            },
22046        };
22047        Ok(Some(out))
22048    }
22049
22050    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
22051    /// predicates, moved out of the tight postfix-cast loop: PG binds
22052    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
22053    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
22054    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
22055    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
22056    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
22057    /// when nothing at this position belongs to the family. Out-of-line
22058    /// (`inline(never)`): the caller sits on the per-nesting-level frame
22059    /// chain that MAX_NEST_DEPTH is tuned against.
22060    #[inline(never)]
22061    fn parse_postfix_predicate(
22062        &mut self,
22063        lhs: &Expr,
22064        min_prec: u8,
22065    ) -> Result<Option<Expr>, ParseError> {
22066        // Reached through try_symbol_operator (an existing leaf call of
22067        // the binary loop) so NO new stack slots land on the per-nesting
22068        // frame chain; the lhs clones only when a predicate actually
22069        // consumes it.
22070        match self.peek() {
22071            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
22072            // comparison family rung 5 (each +1 from the pre-XOR ladder).
22073            Token::Is if min_prec <= 4 => {}
22074            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
22075            Token::Not
22076                if min_prec <= 5
22077                    && matches!(
22078                        self.tokens.get(self.pos + 1),
22079                        Some(Token::Between | Token::In | Token::Like)
22080                    ) => {}
22081            Token::Not | Token::Ident(_)
22082                if min_prec <= 5
22083                    && (matches!(self.peek(), Token::Ident(s)
22084                            if s.eq_ignore_ascii_case("ilike")
22085                                || (self.mysql_dialect
22086                                    && (s.eq_ignore_ascii_case("regexp")
22087                                        || s.eq_ignore_ascii_case("rlike")))
22088                                || (s.eq_ignore_ascii_case("similar")
22089                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
22090                        || (matches!(self.peek(), Token::Not)
22091                            && matches!(self.tokens.get(self.pos + 1),
22092                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
22093                                    || (self.mysql_dialect
22094                                        && (s.eq_ignore_ascii_case("regexp")
22095                                            || s.eq_ignore_ascii_case("rlike")))
22096                                    || s.eq_ignore_ascii_case("similar")))) => {}
22097            _ => return Ok(None),
22098        }
22099        let mut expr = lhs.clone();
22100        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
22101        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
22102        if min_prec <= 4 {
22103            if matches!(self.peek(), Token::Is) {
22104                self.advance();
22105                let negated = if matches!(self.peek(), Token::Not) {
22106                    self.advance();
22107                    true
22108                } else {
22109                    false
22110                };
22111                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
22112                // mailrs pg_dump.
22113                if matches!(self.peek(), Token::Distinct) {
22114                    self.advance();
22115                    if !matches!(self.peek(), Token::From) {
22116                        return Err(self.err(format!(
22117                            "expected FROM after IS{} DISTINCT, got {:?}",
22118                            if negated { " NOT" } else { "" },
22119                            self.peek()
22120                        )));
22121                    }
22122                    self.advance();
22123                    // Right-hand side: parse at the same precedence
22124                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
22125                    // groups as `x IS DISTINCT FROM (a + b)`.
22126                    let rhs = self.parse_expr(5)?;
22127                    let op = if negated {
22128                        BinOp::IsNotDistinctFrom
22129                    } else {
22130                        BinOp::IsDistinctFrom
22131                    };
22132                    expr = Expr::Binary {
22133                        op,
22134                        lhs: Box::new(expr),
22135                        rhs: Box::new(rhs),
22136                    };
22137                    {
22138                        return Ok(Some(expr));
22139                    }
22140                }
22141                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
22142                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
22143                // Lowers onto pg_is_json(x, kind); NOT wraps the
22144                // call in a logical negation.
22145                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22146                if s.eq_ignore_ascii_case("json"))
22147                {
22148                    self.advance(); // JSON
22149                    let kind = match self.peek() {
22150                        Token::Ident(s) | Token::QuotedIdent(s)
22151                            if matches!(
22152                                s.to_ascii_lowercase().as_str(),
22153                                "value" | "object" | "array" | "scalar"
22154                            ) =>
22155                        {
22156                            let k = s.to_ascii_lowercase();
22157                            self.advance();
22158                            k
22159                        }
22160                        _ => "value".to_string(),
22161                    };
22162                    let call = Expr::FunctionCall {
22163                        name: "pg_is_json".to_string(),
22164                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
22165                    };
22166                    expr = if negated {
22167                        Expr::Unary {
22168                            op: UnOp::Not,
22169                            expr: Box::new(call),
22170                        }
22171                    } else {
22172                        call
22173                    };
22174                    {
22175                        return Ok(Some(expr));
22176                    }
22177                }
22178                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
22179                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
22180                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
22181                {
22182                    let form_kw = match self.peek() {
22183                        Token::Ident(s) | Token::QuotedIdent(s)
22184                            if matches!(
22185                                s.to_ascii_uppercase().as_str(),
22186                                "NFC" | "NFD" | "NFKC" | "NFKD"
22187                            ) && matches!(
22188                                self.tokens.get(self.pos + 1),
22189                                Some(Token::Ident(n) | Token::QuotedIdent(n))
22190                                    if n.eq_ignore_ascii_case("normalized")
22191                            ) =>
22192                        {
22193                            Some(s.to_ascii_uppercase())
22194                        }
22195                        _ => None,
22196                    };
22197                    let bare_normalized = form_kw.is_none()
22198                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22199                        if s.eq_ignore_ascii_case("normalized"));
22200                    if form_kw.is_some() || bare_normalized {
22201                        if form_kw.is_some() {
22202                            self.advance(); // form keyword
22203                        }
22204                        self.advance(); // NORMALIZED
22205                        let mut args = alloc::vec![expr];
22206                        if let Some(f) = form_kw {
22207                            args.push(Expr::Literal(Literal::String(f)));
22208                        }
22209                        let call = Expr::FunctionCall {
22210                            name: "is_normalized".to_string(),
22211                            args,
22212                        };
22213                        expr = if negated {
22214                            Expr::Unary {
22215                                op: UnOp::Not,
22216                                expr: Box::new(call),
22217                            }
22218                        } else {
22219                            call
22220                        };
22221                        {
22222                            return Ok(Some(expr));
22223                        }
22224                    }
22225                }
22226                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
22227                // three-valued boolean tests. IS TRUE/FALSE never
22228                // return NULL, so they lower to CASE forms whose
22229                // ELSE catches the NULL branch; IS UNKNOWN on a
22230                // boolean is exactly IS NULL.
22231                if matches!(self.peek(), Token::True | Token::False)
22232                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
22233                {
22234                    let tok = self.advance();
22235                    let test = match tok {
22236                        Token::True => Some(true),
22237                        Token::False => Some(false),
22238                        _ => None, // UNKNOWN
22239                    };
22240                    // v7.39 (round 328, V45) — kept as what the user
22241                    // wrote. These used to be lowered here into `CASE` /
22242                    // `IS NULL`; the semantics were right but the AST no
22243                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
22244                    // was echoed back as
22245                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
22246                    expr = Expr::BoolTest {
22247                        expr: Box::new(expr),
22248                        value: test,
22249                        negated,
22250                    };
22251                    {
22252                        return Ok(Some(expr));
22253                    }
22254                }
22255                if !matches!(self.peek(), Token::Null) {
22256                    return Err(self.err(format!(
22257                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
22258                    if negated { " NOT" } else { "" },
22259                    self.peek()
22260                )));
22261                }
22262                self.advance();
22263                expr = Expr::IsNull {
22264                    expr: Box::new(expr),
22265                    negated,
22266                };
22267                {
22268                    return Ok(Some(expr));
22269                }
22270            }
22271        }
22272        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
22273        if min_prec <= 5 {
22274            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
22275            // Look one token ahead so a stray `NOT` not followed by any of
22276            // these flows through to the early return below untouched.
22277            let negated = if matches!(self.peek(), Token::Not) {
22278                let next = self.tokens.get(self.pos + 1);
22279                matches!(next, Some(Token::Between | Token::In | Token::Like))
22280                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
22281                    || (self.mysql_dialect
22282                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
22283                    || s.eq_ignore_ascii_case("similar"))
22284            } else {
22285                false
22286            };
22287            if negated {
22288                self.advance();
22289            }
22290            if matches!(self.peek(), Token::Between) {
22291                expr = self.parse_between_tail(expr, negated)?;
22292                {
22293                    return Ok(Some(expr));
22294                }
22295            }
22296            if matches!(self.peek(), Token::In) {
22297                if self.suppress_in_tail && !negated {
22298                    // POSITION(sub IN str) — IN belongs to the
22299                    // enclosing function syntax; stop here.
22300                    {
22301                        return Ok(None);
22302                    }
22303                }
22304                expr = self.parse_in_tail(expr, negated)?;
22305                {
22306                    return Ok(Some(expr));
22307                }
22308            }
22309            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
22310            // lowers onto the internal __similar_to(expr, pat[, esc]) call
22311            // (the SQL→regex transform runs inside, in the backtracking-
22312            // friendly shape SPG's matcher needs).
22313            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
22314                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
22315            {
22316                self.advance(); // SIMILAR
22317                self.advance(); // TO
22318                let pattern = self.parse_expr(6)?;
22319                let mut args = alloc::vec![expr, pattern];
22320                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
22321                    self.advance();
22322                    args.push(self.parse_expr(6)?);
22323                }
22324                let call = Expr::FunctionCall {
22325                    name: "__similar_to".to_string(),
22326                    args,
22327                };
22328                expr = maybe_not(call, negated);
22329                {
22330                    return Ok(Some(expr));
22331                }
22332            }
22333            if matches!(self.peek(), Token::Like) {
22334                self.advance();
22335                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
22336                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
22337                    expr = q;
22338                    {
22339                        return Ok(Some(expr));
22340                    }
22341                }
22342                // Pattern at the same precedence as other comparison RHSes —
22343                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
22344                let mut pattern = self.parse_expr(6)?;
22345                // `ESCAPE 'c'` — rewrite a literal pattern to the
22346                // default backslash escape at parse time. Custom
22347                // escapes on non-literal patterns would need
22348                // matcher support; error honestly.
22349                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
22350                    self.advance();
22351                    let esc = self.parse_expr(6)?;
22352                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
22353                }
22354                expr = Expr::Like {
22355                    expr: Box::new(expr),
22356                    pattern: Box::new(pattern),
22357                    negated,
22358                    case_insensitive: false,
22359                };
22360                {
22361                    return Ok(Some(expr));
22362                }
22363            }
22364            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
22365            // keyword reaches us as a plain identifier.
22366            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
22367                self.advance();
22368                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
22369                    expr = q;
22370                    {
22371                        return Ok(Some(expr));
22372                    }
22373                }
22374                let pattern = self.parse_expr(6)?;
22375                expr = Expr::Like {
22376                    expr: Box::new(expr),
22377                    pattern: Box::new(pattern),
22378                    negated,
22379                    case_insensitive: true,
22380                };
22381                {
22382                    return Ok(Some(expr));
22383                }
22384            }
22385            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
22386            // operator (RLIKE is the alias). It is a keyword, not `~`, and
22387            // matches case-insensitively under the default collation, so it
22388            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
22389            // `~*` operator uses, wrapped in NOT when negated.
22390            if self.mysql_dialect
22391                && matches!(self.peek(), Token::Ident(s)
22392                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
22393            {
22394                self.advance();
22395                let pattern = self.parse_expr(6)?;
22396                let call = Expr::FunctionCall {
22397                    name: String::from("regexp_like"),
22398                    args: alloc::vec![
22399                        expr,
22400                        pattern,
22401                        Expr::Literal(Literal::String(String::from("i"))),
22402                    ],
22403                };
22404                return Ok(Some(maybe_not(call, negated)));
22405            }
22406        }
22407        let _ = expr;
22408        Ok(None)
22409    }
22410
22411    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
22412        let mut lhs = self.parse_unary()?;
22413        let mut chain_len = 0usize;
22414        loop {
22415            // OPERATOR([schema.]op) reduces to its underlying
22416            // operator token before the normal dispatch.
22417            let explicit = self.peek_explicit_operator();
22418            let dispatch = match &explicit {
22419                Some((_, tok)) => self.binop_here(tok),
22420                None => self.binop_here(self.peek()),
22421            };
22422            let Some((op, prec)) = dispatch else {
22423                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
22424                // of the symbol family. `binop_here` answers None for them
22425                // because they lower onto function calls rather than a
22426                // BinOp, and the fallback below reads `self.peek()` — the
22427                // word OPERATOR, not the operator. `pg_dump` writes every
22428                // catalog predicate this way, so its first query failed
22429                // and no dump ran:
22430                //
22431                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
22432                //
22433                // Collapsing the wrapper to the operator it names puts the
22434                // token where the fallback already looks.
22435                if let Some((next, op_tok)) = explicit {
22436                    self.tokens.splice(self.pos..next, [op_tok]);
22437                }
22438                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
22439                    lhs = e;
22440                    chain_len += 1;
22441                    if chain_len > MAX_BINARY_CHAIN {
22442                        return Err(self.err(alloc::format!(
22443                            "more than {MAX_BINARY_CHAIN} chained binary operators"
22444                        )));
22445                    }
22446                    continue;
22447                }
22448                break;
22449            };
22450            if prec < min_prec {
22451                break;
22452            }
22453            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
22454            // iteratively but evaluates and drops recursively;
22455            // depth beyond the budget overflows worker stacks.
22456            chain_len += 1;
22457            if chain_len > MAX_BINARY_CHAIN {
22458                return Err(self.err(alloc::format!(
22459                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
22460                )));
22461            }
22462            match explicit {
22463                Some((end_pos, _)) => self.pos = end_pos,
22464                None => {
22465                    self.advance();
22466                }
22467            }
22468            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
22469            // ANY is a bare ident; ALL is a reserved Token. Both
22470            // require an immediate `(` to disambiguate from
22471            // identifier columns named `any` / `all`.
22472            let any_kind = match self.peek() {
22473                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
22474                    Some(false)
22475                }
22476                Token::Ident(s) | Token::QuotedIdent(s)
22477                    if (s.eq_ignore_ascii_case("any")
22478                        || s.eq_ignore_ascii_case("some")
22479                        || s.eq_ignore_ascii_case("all"))
22480                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22481                {
22482                    Some(!s.eq_ignore_ascii_case("all"))
22483                }
22484                _ => None,
22485            };
22486            if let Some(is_any) = any_kind {
22487                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
22488                continue;
22489            }
22490            let rhs = self.parse_expr(prec + 1)?;
22491            lhs = Expr::Binary {
22492                lhs: Box::new(lhs),
22493                op,
22494                rhs: Box::new(rhs),
22495            };
22496        }
22497        Ok(lhs)
22498    }
22499
22500    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
22501    /// and the array form.
22502    ///
22503    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
22504    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
22505    /// this block's `Expr` temporaries and four `format!` sites slots in
22506    /// that frame on every level of `((((1))))`, which never reaches it.
22507    #[inline(never)]
22508    fn parse_any_all_rhs(
22509        &mut self,
22510        lhs: Expr,
22511        op: BinOp,
22512        is_any: bool,
22513    ) -> Result<Expr, ParseError> {
22514        self.advance(); // ident
22515        self.advance(); // (
22516        // `x op ANY (SELECT …)` — the quantified-subquery
22517        // form. `= ANY` is exactly IN; the other operators
22518        // lower onto EXISTS over the subquery as a derived
22519        // table, comparing against its single projection
22520        // aliased __v (x's columns resolve correlated).
22521        // ALL is the negated-EXISTS complement; a NULL
22522        // element makes PG return NULL where this lowering
22523        // returns true — the NOT NULL column case (the
22524        // practical one) is exact.
22525        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
22526            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
22527            // legal PG too (round-151 sibling). Out-of-line
22528            // (#[inline(never)] helper) — this sits on
22529            // parse_expr's recursive frame and the two-armed
22530            // SELECT temporary blew the nesting-budget stack.
22531            let mut sub = self.parse_any_all_select_body()?;
22532            if !matches!(self.peek(), Token::RParen) {
22533                return Err(self.err(alloc::format!(
22534                    "expected ')' after ANY/ALL subquery, got {:?}",
22535                    self.peek()
22536                )));
22537            }
22538            self.advance();
22539            if sub.items.len() != 1 {
22540                return Err(self.err(alloc::format!(
22541                    "ANY/ALL subquery must return one column, got {}",
22542                    sub.items.len()
22543                )));
22544            }
22545            if is_any && matches!(op, BinOp::Eq) {
22546                return Ok(Expr::InSubquery {
22547                    expr: Box::new(lhs),
22548                    subquery: Box::new(sub),
22549                    negated: false,
22550                });
22551            }
22552            // The engine's subquery resolvers materialise
22553            // the single-column result into an ARRAY the
22554            // existing AnyAll three-valued eval consumes.
22555            return Ok(Expr::AnyAll {
22556                expr: Box::new(lhs),
22557                op,
22558                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
22559                is_any,
22560            });
22561        }
22562        let arr = self.parse_expr(0)?;
22563        if !matches!(self.peek(), Token::RParen) {
22564            return Err(self.err(alloc::format!(
22565                "expected ')' after ANY/ALL argument, got {:?}",
22566                self.peek()
22567            )));
22568        }
22569        self.advance();
22570        Ok(Expr::AnyAll {
22571            expr: Box::new(lhs),
22572            op,
22573            array: Box::new(arr),
22574            is_any,
22575        })
22576    }
22577
22578    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
22579    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
22580    #[inline(never)]
22581    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
22582        self.advance();
22583        let e = self.parse_expr(9)?;
22584        Ok(build_center_call(e))
22585    }
22586
22587    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
22588    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
22589    /// unary minus.
22590    ///
22591    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
22592    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
22593    /// the Expr-sized local stays out of that frame.
22594    #[inline(never)]
22595    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
22596        self.advance();
22597        let e = self.parse_expr(9)?;
22598        Ok(Expr::FunctionCall {
22599            name: alloc::string::String::from(name),
22600            args: alloc::vec![e],
22601        })
22602    }
22603
22604    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
22605    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
22606    #[inline(never)]
22607    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
22608        self.advance();
22609        let e = self.parse_expr(9)?;
22610        Ok(Expr::FunctionCall {
22611            name: alloc::string::String::from(if vertical {
22612                "isvertical"
22613            } else {
22614                "ishorizontal"
22615            }),
22616            args: alloc::vec![e],
22617        })
22618    }
22619
22620    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
22621    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
22622    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
22623    #[inline(never)]
22624    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
22625        self.advance();
22626        let e = self.parse_expr(9)?;
22627        Ok(Expr::Cast {
22628            expr: Box::new(e),
22629            target: CastTarget::Named("binary".to_string()),
22630        })
22631    }
22632
22633    /// The prefix operators that share one shape: take the token, parse
22634    /// an operand at `prec`, wrap it.
22635    ///
22636    /// `#[inline(never)]`, and one function instead of five arms, for the
22637    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
22638    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
22639    /// debug build gives EVERY arm's locals a slot in the frame, whichever
22640    /// arm runs. `((((1))))` reaches none of these arms and was carrying
22641    /// five `Expr`-sized locals per level for them anyway.
22642    #[inline(never)]
22643    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
22644        self.advance();
22645        let e = self.parse_expr(prec)?;
22646        Ok(Expr::Unary {
22647            op,
22648            expr: Box::new(e),
22649        })
22650    }
22651
22652    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
22653    /// and separate from it because of the literal folding below and the
22654    /// `format!` temporaries that folding needs.
22655    #[inline(never)]
22656    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
22657        self.advance();
22658        // v7.39 (round 549) — fold the sign into an integer literal that
22659        // only fits once it is negative.
22660        //
22661        // `9223372036854775808` is one past i64::MAX, so the lexer hands
22662        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
22663        // folds the sign first, so `-9223372036854775808` is a bigint
22664        // there — and `-9223372036854775808 - 1` raises "bigint out of
22665        // range" where SPG quietly answered -9223372036854775809, a value
22666        // no bigint can hold. The arithmetic itself was already checked;
22667        // only the literal's type was wrong.
22668        if let Token::Numeric(lit) = self.peek()
22669            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
22670        {
22671            self.advance();
22672            return Ok(Expr::Literal(Literal::Integer(folded)));
22673        }
22674        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
22675        // `<->` slotted into 5 and arithmetic shifted up).
22676        let e = self.parse_expr(9)?;
22677        Ok(Expr::Unary {
22678            op: UnOp::Neg,
22679            expr: Box::new(e),
22680        })
22681    }
22682
22683    /// tsquery `!!` prefix negation, lowered to the catalog function.
22684    /// Binds like unary minus. Out-of-line for the frame reason on
22685    /// `parse_unary_op`.
22686    #[inline(never)]
22687    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
22688        self.advance();
22689        let e = self.parse_expr(9)?;
22690        Ok(Expr::FunctionCall {
22691            name: String::from("tsquery_not"),
22692            args: alloc::vec![e],
22693        })
22694    }
22695
22696    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
22697        match self.peek() {
22698            // NOT binds tighter than AND / XOR / OR but looser than
22699            // comparisons — its operand takes everything ≥ the comparison
22700            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
22701            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
22702            // was rung 3, behaviour-identical when 3 was unused; AND now
22703            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
22704            Token::Not => self.parse_unary_op(UnOp::Not, 4),
22705            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
22706            // The body is out-of-line: `parse_unary` is one of the three
22707            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
22708            // inline arm here overflowed the native stack in
22709            // `nesting_budget_errors_cleanly` — the guard test caught it,
22710            // exactly as the eval-side cliff did in rounds 346 and 351.
22711            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
22712                self.parse_binary_prefix()
22713            }
22714            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
22715            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
22716            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
22717            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
22718            Token::Minus => self.parse_prefix_minus(),
22719            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
22720            // worked only because the lexer reads it as one signed literal;
22721            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
22722            // PG18 and MariaDB take all of them. Binds like unary minus.
22723            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
22724            // Bitwise NOT binds like unary minus.
22725            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
22726            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
22727            // "center of" operator; desugars to center(x). The whole arm
22728            // is out-of-line: parse_unary sits on the per-nesting-level
22729            // frame chain that MAX_NEST_DEPTH is tuned against, so no
22730            // Expr-sized local may live in this frame.
22731            Token::TsMatch => self.parse_prefix_center(),
22732            // v7.39 (round 508) — the prefix operators that are named
22733            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
22734            // is length. Out-of-line for the same nesting-frame reason as
22735            // parse_prefix_center — parse_unary sits on the recursive cycle
22736            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
22737            // live in this frame.
22738            Token::At => self.parse_prefix_call("abs"),
22739            Token::Hash => self.parse_prefix_call("npoints"),
22740            Token::AtMinusAt => self.parse_prefix_call("length"),
22741            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
22742            // "is horizontal" (lseg / line); desugars to the existing
22743            // isvertical()/ishorizontal() functions. Out-of-line for the
22744            // same nesting-frame reason as parse_prefix_center.
22745            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
22746            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
22747            Token::DoubleBang => self.parse_prefix_tsquery_not(),
22748            _ => self.parse_atom(),
22749        }
22750    }
22751
22752    /// Parse a parenthesised scalar subquery body after the caller has consumed
22753    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
22754    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
22755    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
22756    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
22757    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
22758    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
22759    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
22760    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
22761    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
22762    /// tips the deep-nesting test into a stack overflow).
22763    #[inline(never)]
22764    fn array_subquery_ahead(&self) -> bool {
22765        if !matches!(self.peek(), Token::LParen) {
22766            return false;
22767        }
22768        matches!(
22769            self.tokens.get(self.pos + 1),
22770            Some(Token::Select | Token::Values)
22771        ) || matches!(
22772            self.tokens.get(self.pos + 1),
22773            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
22774        )
22775    }
22776
22777    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
22778    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
22779    /// locals stay off parse_atom's recursive frame (round 105).
22780    #[inline(never)]
22781    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
22782        self.advance(); // consume `[`
22783        let mut items: Vec<Expr> = Vec::new();
22784        if !matches!(self.peek(), Token::RBracket) {
22785            loop {
22786                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
22787                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
22788                if matches!(self.peek(), Token::LBracket) {
22789                    items.push(self.parse_array_bracket_body()?);
22790                } else {
22791                    items.push(self.parse_expr(0)?);
22792                }
22793                match self.peek() {
22794                    Token::Comma => {
22795                        self.advance();
22796                    }
22797                    Token::RBracket => break,
22798                    other => {
22799                        return Err(self.err(alloc::format!(
22800                            "expected ',' or ']' in ARRAY literal, got {other:?}"
22801                        )));
22802                    }
22803                }
22804            }
22805        }
22806        self.advance(); // consume `]`
22807        Ok(Expr::Array(items))
22808    }
22809
22810    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
22811    /// is already consumed; the current token is `(`. Desugars to a scalar
22812    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
22813    /// the subquery's single-column rows in order — reusing the existing
22814    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
22815    /// keeps the large `Statement` local off parse_atom's recursive frame.
22816    #[inline(never)]
22817    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
22818        self.advance(); // consume `(`
22819        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
22820            if w.eq_ignore_ascii_case("with"));
22821        let sub = if is_with {
22822            self.advance(); // WITH
22823            self.parse_with_cte_then_select()?
22824        } else {
22825            self.parse_select_stmt()?
22826        };
22827        if !matches!(self.peek(), Token::RParen) {
22828            return Err(self.err(alloc::format!(
22829                "expected ')' to close ARRAY(subquery), got {:?}",
22830                self.peek()
22831            )));
22832        }
22833        self.advance(); // consume `)`
22834        // Reuse the parser to build the array_agg wrapper from the subquery's
22835        // canonical text — avoids hand-constructing the derived-table AST.
22836        let wrapper = alloc::format!(
22837            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
22838        );
22839        let stmt = parse_statement(&wrapper)
22840            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
22841        let Statement::Select(sel) = stmt else {
22842            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
22843        };
22844        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
22845    }
22846
22847    #[inline(never)]
22848    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
22849        let inner = if is_with {
22850            self.advance(); // WITH
22851            self.parse_with_cte_then_select()?
22852        } else {
22853            self.parse_select_stmt()?
22854        };
22855        match self.advance() {
22856            Token::RParen => {
22857                let Statement::Select(s) = inner else {
22858                    return Err(ParseError {
22859                        message: "scalar subquery body must be a SELECT".into(),
22860                        token_pos: self.consumed_pos(),
22861                    });
22862                };
22863                Ok(Expr::ScalarSubquery(Box::new(s)))
22864            }
22865            other => Err(ParseError {
22866                message: format!("expected ')' after scalar subquery, got {other:?}"),
22867                token_pos: self.consumed_pos(),
22868            }),
22869        }
22870    }
22871
22872    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22873    /// literals. The lexer splits them into an ident + string; recombine
22874    /// here. Out-of-line and returning `Option` so `parse_atom` — the
22875    /// recursive frame the 768 KiB stack budget is tuned against — pays no
22876    /// frame for the `body` / `bits` strings and their char loops (the
22877    /// round-367 frame cliff, M20).
22878    #[inline(never)]
22879    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22880        let is_hex = match self.peek() {
22881            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22882            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22883            _ => return None,
22884        };
22885        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22886            return None;
22887        }
22888        // v7.39.3 — where the LITERAL starts, because the errors below
22889        // are about the literal and both engines point at it. `err`
22890        // reports the CURRENT token, which by then is the one after the
22891        // string: `SELECT x'123'` pointed at Eof, so the MySQL wire's
22892        // `near '…'` snippet — which runs from the reported position to
22893        // the end — came out empty where MySQL 9.7.2 says `near
22894        // 'x'123''`.
22895        let lit_pos = self.pos;
22896        self.advance();
22897        let Token::String(body) = self.advance() else {
22898            unreachable!("guarded above");
22899        };
22900        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22901        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22902        // (hex pairs, even count required — MariaDB errors on an odd
22903        // count); `b'1010'` packs its bits big-endian, left-padded to a
22904        // byte. Lower both onto the bytea cast.
22905        if self.mysql_dialect {
22906            if is_hex {
22907                if body.len() % 2 == 1 {
22908                    return Some(Err(self.err_at(
22909                        lit_pos,
22910                        alloc::format!("invalid hex string literal X'{body}': odd digit count"),
22911                    )));
22912                }
22913                for c in body.chars() {
22914                    if !c.is_ascii_hexdigit() {
22915                        return Some(Err(self.err_at(
22916                            lit_pos,
22917                            alloc::format!("invalid hexadecimal digit {c:?} in X'…'"),
22918                        )));
22919                    }
22920                }
22921                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22922            }
22923            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22924                return Some(Err(self.err_at(
22925                    lit_pos,
22926                    alloc::format!("invalid binary digit {bad:?} in b'…'"),
22927                )));
22928            }
22929            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22930        }
22931        let bits = if is_hex {
22932            let mut out = String::with_capacity(body.len() * 4);
22933            for c in body.chars() {
22934                let Some(d) = c.to_digit(16) else {
22935                    // v7.39.3 — PostgreSQL 18.6's own sentence, and its
22936                    // own quoting: `"g" is not a valid hexadecimal
22937                    // digit` (measured, with the caret on the literal).
22938                    return Some(Err(self.err_at(
22939                        lit_pos,
22940                        alloc::format!("\"{c}\" is not a valid hexadecimal digit"),
22941                    )));
22942                };
22943                out.push_str(&alloc::format!("{d:04b}"));
22944            }
22945            out
22946        } else {
22947            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22948                return Some(Err(self.err_at(
22949                    lit_pos,
22950                    alloc::format!("\"{bad}\" is not a valid binary digit"),
22951                )));
22952            }
22953            body
22954        };
22955        // Route through the postfix-cast loop so a chained cast like
22956        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22957        // of erroring at the `::`.
22958        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22959        // literal keeps its exact length, while an explicit `::bit` cast is
22960        // bit(1) with pad/truncate semantics (PG).
22961        Some(self.finish_postfix_casts(Expr::Cast {
22962            expr: Box::new(Expr::Literal(Literal::String(bits))),
22963            target: CastTarget::Named("__bit_literal".to_string()),
22964        }))
22965    }
22966
22967    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22968        if let Some(res) = self.try_parse_bit_string_literal() {
22969            return res;
22970        }
22971        // v7.40.11 — `<alias>.*` in an EXPRESSION is the whole row, the
22972        // same thing the bare alias is.
22973        //
22974        // Reported against 7.40.9: `to_jsonb(t.*)`, `row_to_json(t.*)`,
22975        // `pg_column_size(t.*)` and `count(t.*)` were all
22976        // `syntax error at or near "*"`, while `to_jsonb(t)` — the same
22977        // value, differently spelled — worked. Measured on PG 18.6, the
22978        // two spellings answer byte-identically:
22979        //
22980        //   to_jsonb(t.*)   {"a": 1, "b": "x"}
22981        //   to_jsonb(t)     {"a": 1, "b": "x"}
22982        //
22983        // Here rather than in the argument list: nothing else in an
22984        // expression position is `ident . *`, and a select item's own
22985        // `t.*` is recognised before `parse_expr` is ever called, so
22986        // `SELECT t.* FROM t` keeps expanding to the column list.
22987        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone()
22988            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22989            && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
22990        {
22991            self.advance();
22992            self.advance();
22993            self.advance();
22994            return Ok(Expr::Column(ColumnName {
22995                qualifier: None,
22996                name: q,
22997            }));
22998        }
22999        let tok_pos = self.pos;
23000        match self.advance() {
23001            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
23002            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
23003            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
23004            // carrying the source mantissa + scale so no precision is lost. A
23005            // literal too wide for i128 falls back to double precision.
23006            // Out-of-line (#[inline(never)]) — this arm sits on the
23007            // parse_expr recursion chain; its expansion locals must not
23008            // widen the recursive frame (debug frame-cliff discipline).
23009            Token::Numeric(s) => match numeric_token_to_literal(s) {
23010                Ok(lit) => Ok(Expr::Literal(lit)),
23011                Err(msg) => Err(self.err(msg)),
23012            },
23013            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
23014            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
23015            // (the lexer only emits this token in the MySQL dialect). Lower
23016            // onto the existing bytea cast; out-of-line to keep this arm off
23017            // the parse recursion frame.
23018            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
23019            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
23020            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
23021            Token::Null => Ok(Expr::Literal(Literal::Null)),
23022            // v6.1.1 — `$N` placeholder. The actual Value lookup
23023            // happens in the engine eval path against the prepared-
23024            // statement bind buffer.
23025            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
23026            Token::LParen => {
23027                // v4.10: `(SELECT ...)` in expression position is a
23028                // scalar subquery; otherwise it's a parenthesised
23029                // expression. Peek for SELECT keyword to dispatch.
23030                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
23031                // lexes as Ident("with") (not a reserved token). The subquery body
23032                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
23033                // so its large `Statement` local stays out of parse_atom's stack
23034                // frame — parse_atom is on the recursive `((…))` cycle and the
23035                // nesting budget is tuned to its frame size).
23036                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23037                    if s.eq_ignore_ascii_case("with"));
23038                if matches!(self.peek(), Token::Select) || is_with {
23039                    self.parse_paren_scalar_subquery(is_with)
23040                } else {
23041                    let e = self.parse_expr(0)?;
23042                    // `(a, b, …)` — a row constructor. Valid only
23043                    // in front of a comparison operator or [NOT]
23044                    // IN; both expand at parse time (lexicographic
23045                    // comparison / OR'd row equalities).
23046                    if matches!(self.peek(), Token::Comma) {
23047                        let mut row = alloc::vec![e];
23048                        while matches!(self.peek(), Token::Comma) {
23049                            self.advance();
23050                            row.push(self.parse_expr(0)?);
23051                        }
23052                        if !matches!(self.peek(), Token::RParen) {
23053                            return Err(self.err(alloc::format!(
23054                                "expected ')' after row constructor, got {:?}",
23055                                self.peek()
23056                            )));
23057                        }
23058                        self.advance();
23059                        // A bare `(a, b, …)` row constructor can carry postfix
23060                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
23061                        // early return here skips parse_atom's tail postfix
23062                        // pass, so fold casts in explicitly. For the
23063                        // comparison / predicate forms nothing postfix follows,
23064                        // so this is a no-op.
23065                        return self
23066                            .parse_row_comparison_tail(row)
23067                            .and_then(|e| self.finish_postfix_casts(e));
23068                    }
23069                    match self.advance() {
23070                        Token::RParen => Ok(e),
23071                        other => Err(ParseError {
23072                            message: format!("expected ')', got {other:?}"),
23073                            token_pos: self.consumed_pos(),
23074                        }),
23075                    }
23076                }
23077            }
23078            Token::LBracket => self.parse_vector_literal_body(),
23079            Token::Extract => self.parse_extract_atom(),
23080            Token::Interval => self.parse_interval_atom(),
23081            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
23082            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
23083            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
23084            // expression position calling the PG `left(string, n)` /
23085            // `right(string, n)` function; rebuild the AST as a regular
23086            // function call so the engine's apply_function dispatch picks
23087            // it up. Delegated to a #[inline(never)] helper so its locals
23088            // don't bloat this recursive `parse_atom` frame (the nesting
23089            // budget in `enter_nested` is tuned to parse_atom's size).
23090            Token::Left if matches!(self.peek(), Token::LParen) => {
23091                self.parse_lr_string_function_call("left")
23092            }
23093            Token::Right if matches!(self.peek(), Token::LParen) => {
23094                self.parse_lr_string_function_call("right")
23095            }
23096            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
23097            // token; we match on the bare ident. NOT is a token
23098            // (consumed in the comparison rung), but `EXISTS (...)`
23099            // at the top of an expression starts here.
23100            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
23101                self.parse_exists_atom(false)
23102            }
23103            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
23104            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
23105            // CASE is a bare ident; we dispatch on lowercase match.
23106            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
23107                self.parse_case_atom()
23108            }
23109            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
23110            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
23111            // '…'`. Lower onto the ::cast node so the existing
23112            // runtime text→date/timestamp paths do the parsing. The
23113            // string must follow immediately, else the ident stays a
23114            // plain column reference.
23115            Token::Ident(s)
23116                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
23117                    && matches!(self.peek(), Token::String(_)) =>
23118            {
23119                let target =
23120                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
23121                let Token::String(lit) = self.advance() else {
23122                    unreachable!("peek guaranteed a string token");
23123                };
23124                Ok(Expr::Cast {
23125                    expr: Box::new(Expr::Literal(Literal::String(lit))),
23126                    target,
23127                })
23128            }
23129            // v7.39 (round 221) — the SQL-standard long spellings:
23130            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
23131            // TIME ZONE '…'`. Consume the modifier and lower to the same
23132            // typed-literal cast (`timetz` / `timestamptz` for WITH).
23133            Token::Ident(s)
23134                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
23135                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
23136                        || w.eq_ignore_ascii_case("without"))
23137                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
23138                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
23139                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
23140            {
23141                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
23142                self.advance(); // WITH / WITHOUT
23143                self.advance(); // TIME
23144                self.advance(); // ZONE
23145                let Token::String(lit) = self.advance() else {
23146                    unreachable!("guard checked a string token");
23147                };
23148                let base = s.to_ascii_lowercase();
23149                let target = match (base.as_str(), with_tz) {
23150                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
23151                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
23152                    (_, true) => CastTarget::Timestamptz,
23153                    (_, false) => CastTarget::Timestamp,
23154                };
23155                Ok(Expr::Cast {
23156                    expr: Box::new(Expr::Literal(Literal::String(lit))),
23157                    target,
23158                })
23159            }
23160            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
23161            // gathers the subquery's single-column rows (in its row order)
23162            // into an array. Desugared to `array_agg` over the subquery as a
23163            // derived table; out-of-line to keep parse_atom's frame small (it
23164            // sits on the recursive nesting-budget cycle).
23165            Token::Ident(s) | Token::QuotedIdent(s)
23166                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
23167            {
23168                self.parse_array_subquery()
23169            }
23170            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
23171            // is not a reserved token; we match by case-insensitive
23172            // ident. The opening `[` must follow immediately. v7.39 (read01
23173            // round 105) — the body moved out-of-line so its `Vec`/loop locals
23174            // leave parse_atom's frame (which sits on the nesting-budget cycle).
23175            Token::Ident(s) | Token::QuotedIdent(s)
23176                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
23177            {
23178                self.parse_array_literal_body()
23179            }
23180            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
23181            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
23182            // We special-case before the generic ident dispatch so
23183            // the AGAINST clause never reaches the function-call
23184            // loop (which would mis-read `(cols) AGAINST` as a
23185            // call with no trailing modifier). The shape is
23186            // rewritten to a Boolean OR over per-column
23187            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
23188            // term)` so the existing FTS evaluator handles
23189            // semantics — the fulltext-GIN built at CREATE TABLE
23190            // time is currently a "real index that survives dump
23191            // round-trip"; the planner hook that actually uses
23192            // it for posting-list intersection lands in a later
23193            // sub-phase (Phase 2.2b) without touching this surface.
23194            Token::Ident(s) | Token::QuotedIdent(s)
23195                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
23196            {
23197                self.parse_match_against_atom()
23198            }
23199            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
23200            // v7.37.43-T4 — PG-unreserved keywords are legal column /
23201            // alias names in expression context too. `release` appears
23202            // in sentori `0003_partition_events.sql` as both a column
23203            // reference (SELECT … release …) and an INSERT column list
23204            // entry. Mirrors `expect_ident_like`'s expansion of the
23205            // identifier set.
23206            other if unreserved_keyword_text(&other).is_some() => {
23207                let s = unreserved_keyword_text(&other).unwrap();
23208                self.finish_ident_atom(s)
23209            }
23210            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
23211            // only inside `SET` before, so `SELECT @@autocommit` — which
23212            // every MySQL connector asks at handshake — was a parse error.
23213            // MariaDB accepts the bare, `@@session.` and `@@global.`
23214            // spellings alike and answers from the session's own value.
23215            Token::SessionVar(v) => {
23216                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
23217                // has nothing to do with a `@@` engine setting: its own
23218                // per-session namespace, and an unset one reads NULL instead
23219                // of raising. Stripping every `@` (as this did) made `@x` and
23220                // `@@x` the same node, so `SELECT @x` answered "Unknown
23221                // system variable".
23222                Ok(variable_ref_atom(&v))
23223            }
23224            other => Err(ParseError {
23225                message: format!("unexpected token {other:?} in expression"),
23226                token_pos: tok_pos,
23227            }),
23228        }
23229        // After parsing the atom, fold any postfix `::vector` casts.
23230        .and_then(|atom| self.finish_postfix_casts(atom))
23231    }
23232
23233    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
23234    /// Both bind tighter than any binary op.
23235    /// Shared cast-target parser for postfix `::TYPE` and the
23236    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
23237    /// If the next tokens are `( N )`, consume them and return the canonical
23238    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
23239    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
23240    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
23241        if !matches!(self.peek(), Token::LParen) {
23242            return None;
23243        }
23244        self.advance(); // (
23245        let n = match self.advance() {
23246            Token::Integer(n) => n,
23247            _ => return Some(base.to_string()), // malformed → drop precision
23248        };
23249        if matches!(self.peek(), Token::RParen) {
23250            self.advance();
23251        }
23252        Some(alloc::format!("{base}({n})"))
23253    }
23254
23255    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
23256        // r1052 — `::pg_catalog.regproc` and friends: pg_dump
23257        // schema-qualifies every cast target, and `pg_catalog.X` names
23258        // exactly the builtin type X. Consume the qualifier and let
23259        // the ordinary target parse decide.
23260        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
23261            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
23262        {
23263            self.advance();
23264            self.advance();
23265        }
23266        let target = match self.advance() {
23267            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
23268                "int" | "integer" | "int4" => {
23269                    if matches!(self.peek(), Token::LBracket)
23270                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23271                    {
23272                        self.advance();
23273                        self.advance();
23274                        CastTarget::IntArray
23275                    } else {
23276                        CastTarget::Int
23277                    }
23278                }
23279                "bigint" | "int8" => {
23280                    if matches!(self.peek(), Token::LBracket)
23281                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23282                    {
23283                        self.advance();
23284                        self.advance();
23285                        CastTarget::BigIntArray
23286                    } else {
23287                        CastTarget::BigInt
23288                    }
23289                }
23290                "float" | "double" => CastTarget::Float,
23291                "text" => {
23292                    // v7.10.11 — `::TEXT[]` widens to TextArray.
23293                    if matches!(self.peek(), Token::LBracket)
23294                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23295                    {
23296                        self.advance();
23297                        self.advance();
23298                        CastTarget::TextArray
23299                    } else {
23300                        CastTarget::Text
23301                    }
23302                }
23303                "bool" | "boolean" => CastTarget::Bool,
23304                "vector" => CastTarget::Vector,
23305                "date" => CastTarget::Date,
23306                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
23307                // seconds precision through the Named path (the engine rounds
23308                // the sub-second field); bare `::timestamp` keeps the fast arm.
23309                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
23310                    Some(named) => CastTarget::Named(named),
23311                    None => CastTarget::Timestamp,
23312                },
23313                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
23314                    Some(named) => CastTarget::Named(named),
23315                    None => CastTarget::Timestamptz,
23316                },
23317                "interval" => CastTarget::Interval,
23318                "json" => CastTarget::Json,
23319                "jsonb" => CastTarget::Jsonb,
23320                // v7.39 (round 694) — these have dedicated CastTarget
23321                // variants, so they never reached the postfix `[]` handling
23322                // further down and `::regtype[]` was a SYNTAX error at the
23323                // `]`. PG has an array type for every scalar; take the
23324                // suffix here and hand the canonical `<ty>_array` name to
23325                // the engine, the same shape every other array cast uses.
23326                "regtype" if self.peek_postfix_array_brackets() => {
23327                    self.advance();
23328                    self.advance();
23329                    CastTarget::Named(alloc::string::String::from("regtype_array"))
23330                }
23331                "regclass" if self.peek_postfix_array_brackets() => {
23332                    self.advance();
23333                    self.advance();
23334                    CastTarget::Named(alloc::string::String::from("regclass_array"))
23335                }
23336                "regtype" => CastTarget::RegType,
23337                "regclass" => CastTarget::RegClass,
23338                // v7.12.0 — `::tsvector` / `::tsquery`.
23339                // Engine decodes the LHS text via the PG
23340                // external form parser.
23341                // v7.39 (round 352, M8) — MySQL's own cast targets.
23342                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
23343                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
23344                // such type, so they are taken only in that dialect and
23345                // fall through to the "type does not exist" arm otherwise.
23346                "signed" | "unsigned" if self.mysql_dialect => {
23347                    if matches!(self.peek(), Token::Ident(k)
23348                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
23349                    {
23350                        self.advance();
23351                    }
23352                    CastTarget::Named(s.to_ascii_lowercase())
23353                }
23354                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
23355                // in MySQL: MariaDB answers '123' where the SQL-standard
23356                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
23357                // Truncating a number to its first digit is a wrong answer
23358                // with no error, so the MySQL session gets MySQL's reading.
23359                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
23360                    CastTarget::Text
23361                }
23362                "tsvector" => CastTarget::TsVector,
23363                "tsquery" => CastTarget::TsQuery,
23364                // v7.17.0 — `::uuid`. Engine decodes the LHS
23365                // text via `spg_storage::parse_uuid_str`.
23366                "uuid" => CastTarget::Uuid,
23367                // v7.18 — `::bytea`. Engine decodes the LHS
23368                // text via the PG hex form (`'\xdeadbeef'`)
23369                // or escape form (`'\\x05\\x00'`). Closes
23370                // mailrs D-pre #3 reverse-acceptance gap.
23371                "bytea" => CastTarget::Bytea,
23372                // v7.37.5 ship triage — generic typed-cast escape.
23373                // Anything the long-tail PG type ident table knows
23374                // about(network/bit/geometry/multirange/etc.)flows
23375                // through `CastTarget::Named(canonical)`; the engine
23376                // resolves via `column_type_to_data_type` and dispatches
23377                // through the typed `coerce_value` path. Truly
23378                // unrecognised idents still hit the error arm below
23379                // because the engine rejects them.
23380                other => {
23381                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
23382                    // `::varchar(255)`, etc. Capture into the canonical
23383                    // `name(p,s)` form so `type_name_to_data_type` can
23384                    // reconstruct the `DataType::Numeric { precision,
23385                    // scale }` (and similar param-carrying types).
23386                    let mut name = other.to_string();
23387                    // v7.39 (round 281) — `::bit varying(3)` is two
23388                    // words; fold the tail in so the typmod reaches the
23389                    // type resolver instead of tripping the parser.
23390                    if name.eq_ignore_ascii_case("bit")
23391                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
23392                    {
23393                        self.advance();
23394                        name = alloc::string::String::from("varbit");
23395                    }
23396                    // v7.39 (round 613) — `::character varying` is the same
23397                    // two-word shape and had no fold, so the `varying` was
23398                    // left behind and the cast became a bare `character`,
23399                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
23400                    // `a` where PG answers `ab`. Silently, and for a spelling
23401                    // pg_dump writes.
23402                    if name.eq_ignore_ascii_case("character")
23403                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
23404                    {
23405                        self.advance();
23406                        name = alloc::string::String::from("varchar");
23407                    }
23408                    if matches!(self.peek(), Token::LParen) {
23409                        let mut buf = alloc::string::String::from("(");
23410                        let mut depth = 0usize;
23411                        loop {
23412                            match self.advance() {
23413                                Token::LParen => {
23414                                    depth += 1;
23415                                    if depth > 1 {
23416                                        buf.push('(');
23417                                    }
23418                                }
23419                                Token::RParen => {
23420                                    depth -= 1;
23421                                    if depth == 0 {
23422                                        buf.push(')');
23423                                        break;
23424                                    }
23425                                    buf.push(')');
23426                                }
23427                                Token::Comma => buf.push(','),
23428                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
23429                                // v7.39 (round 273) — a minus used to fall
23430                                // into the catch-all below and vanish, so
23431                                // `::numeric(10,-2)` reached the engine as
23432                                // the text `numeric(10,2)` and silently
23433                                // rounded to two DECIMALS instead of to
23434                                // hundreds. A dropped token is not a
23435                                // no-op when it carries a sign.
23436                                Token::Minus => buf.push('-'),
23437                                Token::Eof => break,
23438                                _ => {}
23439                            }
23440                        }
23441                        name.push_str(&buf);
23442                    }
23443                    // Optional postfix `[]` widens to the array form —
23444                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
23445                    // The engine's `type_name_to_data_type` recognises
23446                    // the canonical `<ty>_array` form.
23447                    if matches!(self.peek(), Token::LBracket)
23448                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23449                    {
23450                        self.advance();
23451                        self.advance();
23452                        name.push_str("_array");
23453                    }
23454                    CastTarget::Named(name)
23455                }
23456            },
23457            Token::Interval => CastTarget::Interval,
23458            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
23459            // "char" (oid 18, SPG Char1 — distinct from bare `char`
23460            // = char(1)); other quoted names resolve like idents.
23461            Token::QuotedIdent(q) => {
23462                if q.eq_ignore_ascii_case("char") {
23463                    CastTarget::Named("char1".into())
23464                } else {
23465                    CastTarget::Named(q.to_ascii_lowercase())
23466                }
23467            }
23468            other => {
23469                return Err(ParseError {
23470                    message: format!("expected type ident after `::`, got {other:?}"),
23471                    token_pos: self.consumed_pos(),
23472                });
23473            }
23474        };
23475        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
23476        // target to its array sibling. Closed-enum arms (Bool /
23477        // SmallInt / Numeric / Float / Date / …) didn't carry the
23478        // explicit widening that Text / Int / BigInt did, so
23479        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
23480        // error. The widening here mirrors the per-arm Text /
23481        // Int / BigInt logic above + folds the new ζ-A first-class
23482        // types through `CastTarget::Named("<ty>_array")`.
23483        if matches!(self.peek(), Token::LBracket)
23484            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23485        {
23486            let widened = match &target {
23487                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
23488                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
23489                // v7.39 (round 326, V43) — the two temporal types stay
23490                // distinct. Both used to widen to `timestamptz_array`, so
23491                // `::timestamp[]` named the wrong target in its own error
23492                // message and lost the zone-less identity on the way.
23493                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
23494                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
23495                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
23496                CastTarget::Json | CastTarget::Jsonb => {
23497                    Some(CastTarget::Named("jsonb_array".to_string()))
23498                }
23499                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
23500                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
23501                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
23502                CastTarget::Named(name) => {
23503                    let mut a = name.clone();
23504                    a.push_str("_array");
23505                    Some(CastTarget::Named(a))
23506                }
23507                // Int / BigInt / Text / Vector / TsVector / TsQuery /
23508                // RegType / RegClass / TextArray / IntArray /
23509                // BigIntArray already finalised — leave as is.
23510                _ => None,
23511            };
23512            if let Some(w) = widened {
23513                self.advance();
23514                self.advance();
23515                return Ok(w);
23516            }
23517        }
23518        Ok(target)
23519    }
23520
23521    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
23522        loop {
23523            // v7.38 (read01, T9) — composite field access `(expr).field`.
23524            // A bare `a.b` is consumed as a qualified column inside the ident
23525            // atom, so a Dot only survives to this postfix position when the
23526            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
23527            // `.*` whole-row expansion is not handled here (projection-level).
23528            if matches!(self.peek(), Token::Dot)
23529                && matches!(
23530                    self.tokens.get(self.pos + 1),
23531                    Some(Token::Ident(_) | Token::QuotedIdent(_))
23532                )
23533            {
23534                self.advance(); // .
23535                let field = match self.advance() {
23536                    Token::Ident(s) | Token::QuotedIdent(s) => s,
23537                    other => {
23538                        return Err(
23539                            self.err(format!("expected a field name after '.', got {other:?}"))
23540                        );
23541                    }
23542                };
23543                expr = Expr::FieldAccess {
23544                    base: Box::new(expr),
23545                    field,
23546                };
23547                continue;
23548            }
23549            if matches!(self.peek(), Token::DoubleColon) {
23550                self.advance();
23551                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
23552                // target set to include INTERVAL (reserved Token),
23553                // TIMESTAMPTZ, and PG catalog regtype / regclass.
23554                // mailrs follow-up H3a + H3b.
23555                let target = self.parse_cast_target()?;
23556                expr = Expr::Cast {
23557                    expr: Box::new(expr),
23558                    target,
23559                };
23560                continue;
23561            }
23562            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
23563            // returns NULL for out-of-range. Multiple subscripts
23564            // chain: `a[i][j]` parses left-to-right.
23565            if matches!(self.peek(), Token::LBracket) {
23566                self.advance();
23567                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
23568                // bare index stays a subscript.
23569                let lo = if matches!(self.peek(), Token::Colon) {
23570                    None
23571                } else {
23572                    Some(self.parse_expr(0)?)
23573                };
23574                if matches!(self.peek(), Token::Colon) {
23575                    self.advance();
23576                    let hi = if matches!(self.peek(), Token::RBracket) {
23577                        None
23578                    } else {
23579                        Some(Box::new(self.parse_expr(0)?))
23580                    };
23581                    if !matches!(self.peek(), Token::RBracket) {
23582                        return Err(self.err(alloc::format!(
23583                            "expected ']' after array slice, got {:?}",
23584                            self.peek()
23585                        )));
23586                    }
23587                    self.advance();
23588                    expr = Expr::ArraySlice {
23589                        target: Box::new(expr),
23590                        lo: lo.map(Box::new),
23591                        hi,
23592                    };
23593                    continue;
23594                }
23595                let index = lo.expect("non-colon branch parsed an index");
23596                if !matches!(self.peek(), Token::RBracket) {
23597                    return Err(self.err(alloc::format!(
23598                        "expected ']' after array index, got {:?}",
23599                        self.peek()
23600                    )));
23601                }
23602                self.advance();
23603                expr = Expr::ArraySubscript {
23604                    target: Box::new(expr),
23605                    index: Box::new(index),
23606                };
23607                continue;
23608            }
23609            // `expr AT TIME ZONE zone` — lowers to PG's own function
23610            // form timezone(zone, expr); the scalar implements the
23611            // offset shift (named zones error there — no tzdata).
23612            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
23613                && matches!(self.tokens.get(self.pos + 1),
23614                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
23615                && matches!(self.tokens.get(self.pos + 2),
23616                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
23617            {
23618                self.advance(); // AT
23619                self.advance(); // TIME
23620                self.advance(); // ZONE
23621                // Zone at comparison precedence so AND/OR stay out.
23622                let zone = self.parse_expr(6)?;
23623                expr = Expr::FunctionCall {
23624                    name: "timezone".to_string(),
23625                    args: alloc::vec![zone, expr],
23626                };
23627                continue;
23628            }
23629            // `expr COLLATE "name"` — SPG's single text ordering IS
23630            // byte order, i.e. the C collation. The byte-order
23631            // spellings absorb as no-ops; a locale collation would
23632            // silently sort differently from PG, so it errors
23633            // honestly instead.
23634            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
23635                self.advance();
23636                let mut cname = match self.advance() {
23637                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23638                    other => {
23639                        return Err(self.err(alloc::format!(
23640                            "expected collation name after COLLATE, got {other:?}"
23641                        )));
23642                    }
23643                };
23644                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
23645                // is how `pg_dump` writes the default one:
23646                // `… COLLATE pg_catalog.default`. Reading a single token
23647                // left the SCHEMA as the name, so the clause was refused
23648                // as an unsupported locale collation and no dump ran.
23649                if matches!(self.peek(), Token::Dot) {
23650                    // v7.39.2 — the qualifier is DROPPED (SPG is single
23651                    // schema) but it is checked first. PostgreSQL 18.6
23652                    // answers `schema "nosuch_schema" does not exist` for
23653                    // one it has never heard of, and dropping it unread
23654                    // meant `COLLATE nosuch_schema."C"` succeeded here —
23655                    // a name that names nothing, accepted.
23656                    let schema = cname.to_ascii_lowercase();
23657                    if !matches!(
23658                        schema.as_str(),
23659                        "pg_catalog" | "public" | "information_schema"
23660                    ) {
23661                        return Err(self.err(alloc::format!("schema \"{cname}\" does not exist")));
23662                    }
23663                    self.advance();
23664                    cname = match self.advance() {
23665                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23666                        // `default` lexes as a KEYWORD, and it is the name
23667                        // pg_dump writes — the same trap round 535 hit with
23668                        // TABLE / INDEX / FULL.
23669                        Token::Default => alloc::string::String::from("default"),
23670                        other => {
23671                            return Err(self.err(alloc::format!(
23672                                "expected collation name after COLLATE, got {other:?}"
23673                            )));
23674                        }
23675                    };
23676                }
23677                let lc = cname.to_ascii_lowercase();
23678                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
23679                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
23680                // family / `binary`) forces byte-wise, which is exactly
23681                // what `BINARY expr` does — lower onto that so every fold
23682                // site (comparison, LIKE, ORDER BY) suppresses via
23683                // `is_binary_coerced`. A `_ci` family override folds, and
23684                // under the MySQL dialect the default already folds, so it
23685                // absorbs as a no-op; likewise the C / byte-order spellings.
23686                // v7.39.2 — against MySQL's own list, not against the
23687                // shape of the name. `nosuch_bin` took this shortcut and
23688                // became a BINARY cast; `nosuch_ci` took the one below
23689                // and was absorbed as a no-op. Either way the client
23690                // named a collation that does not exist and was told
23691                // nothing. An unknown name now falls through to the
23692                // node, and the engine refuses it.
23693                let real = crate::charset::is_mysql_collation(&lc);
23694                // v7.40.0 — `binary` lowers; a `_bin` COLLATION does not.
23695                //
23696                // They are not the same thing, and folding them together
23697                // lost a bit. Measured on MySQL 9.7.2 with the connection
23698                // on utf8mb4:
23699                //
23700                // ```text
23701                //   'a ' = 'a' COLLATE utf8mb4_bin        1   PAD SPACE
23702                //   'a ' = 'a' COLLATE utf8mb4_0900_bin   0   NO PAD
23703                //   'AB' = 'ab' COLLATE utf8mb4_bin       0   byte-wise
23704                // ```
23705                //
23706                // The BINARY cast carries "byte-wise" and, with it,
23707                // "no pad" — so `utf8mb4_bin`, which pads, answered 0 to
23708                // the first line. Keeping the node lets `text_compare_of`
23709                // read the NAME and settle the two bits separately: it
23710                // does not fold (`folds_case` says so) and it does pad
23711                // (`pads_space` says so), while `is_byte_wise` still
23712                // keeps the ORDERING off the locale.
23713                if self.mysql_dialect && real && lc == "binary" {
23714                    expr = Expr::Cast {
23715                        expr: alloc::boxed::Box::new(expr),
23716                        target: CastTarget::Named("binary".to_string()),
23717                    };
23718                    continue;
23719                }
23720                let mysql_ci = self.mysql_dialect
23721                    && ((real && lc.ends_with("_ci"))
23722                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
23723                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
23724                // goes to the lowering channel, the byte-order spellings
23725                // included. Round 691 recorded only the names the old
23726                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
23727                // absorbed as a no-op — and once a column could declare a
23728                // collation, absorbing the clause meant the COLUMN's
23729                // collation won where the query had asked for bytes.
23730                if self.in_order_by_key && !mysql_ci {
23731                    self.order_key_collation = Some(cname);
23732                    continue;
23733                }
23734                // v7.39.2 — the clause becomes a NODE rather than being
23735                // refused or absorbed.
23736                //
23737                // What stood here refused the locale names and SILENTLY
23738                // DROPPED the byte-order ones, so `'a' COLLATE "C" < 'B'`
23739                // answered `t` where PostgreSQL 18.6 answers `f`: the one
23740                // family it let through is the one where dropping it
23741                // changes the answer. Absorbing is only correct when the
23742                // collation asked for is the one the comparison would use
23743                // anyway, and that depends on the DATABASE — which the
23744                // parser cannot see. So it rides along and the engine,
23745                // which can, decides.
23746                //
23747                // `collate_derive` already modelled `Explicit(name)` and
23748                // had no way to be handed one.
23749                // v7.39.2 — a MySQL spelling does not exist on the
23750                // PostgreSQL wire, and THIS is where the wire is known.
23751                //
23752                // The check lived in the evaluator first and asked
23753                // `ctx.mysql_dialect`, which the INSERT path builds as a
23754                // hard-coded `false` — so `INSERT … VALUES (_utf8mb4'x')`
23755                // in a MySQL session was refused for a collation that
23756                // does not exist on a wire it was not on. Making that
23757                // context truthful would change INSERT-time evaluation
23758                // in other ways as a side effect; the parser already
23759                // gates the introducer on the same flag and is the
23760                // honest place to ask.
23761                if !self.mysql_dialect
23762                    && (lc.ends_with("_ci")
23763                        || lc.ends_with("_cs")
23764                        || lc.ends_with("_bin")
23765                        || lc == "binary"
23766                        || matches!(lc.as_str(), "case_insensitive" | "nocase"))
23767                {
23768                    return Err(self.err(alloc::format!(
23769                        "collation \"{cname}\" for encoding \"UTF8\" does not exist"
23770                    )));
23771                }
23772                // v7.39.3 — the node is built for EVERY name, `_ci`
23773                // included.
23774                //
23775                // A MySQL `_ci` spelling used to be absorbed here on the
23776                // reasoning that a MySQL session folds anyway, so the
23777                // clause asked for what it would have got. That stopped
23778                // being true when the fold learned to read the session's
23779                // collation NAME: under `SET NAMES utf8mb4 COLLATE
23780                // utf8mb4_bin`, `'AB' COLLATE utf8mb4_general_ci = 'ab'`
23781                // is 1 on MySQL 9.7.2 and was 0 here, because the clause
23782                // that would have made it 1 had been dropped in the
23783                // parser. Absorbing is only ever correct when the
23784                // collation asked for is the one the comparison would use
23785                // anyway, and the parser cannot know that — the same
23786                // reasoning already written above for the byte-order
23787                // spellings, applied to the family it had exempted.
23788                expr = Expr::Collate {
23789                    expr: alloc::boxed::Box::new(expr),
23790                    collation: cname,
23791                };
23792                continue;
23793            }
23794            return Ok(expr);
23795        }
23796    }
23797
23798    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
23799    /// the first token that is not one. Schema qualifiers collapse to the
23800    /// last part, which is what every other name path here does (SPG is
23801    /// single-schema).
23802    fn take_comma_separated_names(&mut self) -> Vec<String> {
23803        let mut out = Vec::new();
23804        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
23805            self.advance();
23806            let mut last = n;
23807            while matches!(self.peek(), Token::Dot) {
23808                self.advance();
23809                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
23810                    last = t;
23811                }
23812            }
23813            out.push(last);
23814            if matches!(self.peek(), Token::Comma) {
23815                self.advance();
23816            } else {
23817                break;
23818            }
23819        }
23820        out
23821    }
23822
23823    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
23824    ///
23825    /// The general cast-target path tests this inline; the types with their
23826    /// own `CastTarget` variant need it as a guard on their match arm,
23827    /// which is what this exists for.
23828    fn peek_postfix_array_brackets(&self) -> bool {
23829        matches!(self.peek(), Token::LBracket)
23830            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23831    }
23832
23833    /// Parse the operator tail after a `(a, b, …)` row constructor
23834    /// and expand at parse time. `=` is the conjunction of element
23835    /// equalities; `<>` its negation; the order operators expand
23836    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
23837    /// equalities. Anything else (a bare row value, a subquery
23838    /// RHS) errors honestly — SPG has no composite runtime value.
23839    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
23840        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
23841            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
23842                lhs: Box::new(l.clone()),
23843                op: BinOp::Eq,
23844                rhs: Box::new(r.clone()),
23845            });
23846            let first = it.next().expect("row has at least two elements");
23847            it.fold(first, |acc, e| Expr::Binary {
23848                lhs: Box::new(acc),
23849                op: BinOp::And,
23850                rhs: Box::new(e),
23851            })
23852        }
23853        // Lexicographic (a,b) OP (c,d):
23854        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
23855        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
23856            if lhs.len() == 1 {
23857                return Expr::Binary {
23858                    lhs: Box::new(lhs[0].clone()),
23859                    op: last,
23860                    rhs: Box::new(rhs[0].clone()),
23861                };
23862            }
23863            let head_strict = Expr::Binary {
23864                lhs: Box::new(lhs[0].clone()),
23865                op: strict,
23866                rhs: Box::new(rhs[0].clone()),
23867            };
23868            let head_eq = Expr::Binary {
23869                lhs: Box::new(lhs[0].clone()),
23870                op: BinOp::Eq,
23871                rhs: Box::new(rhs[0].clone()),
23872            };
23873            Expr::Binary {
23874                lhs: Box::new(head_strict),
23875                op: BinOp::Or,
23876                rhs: Box::new(Expr::Binary {
23877                    lhs: Box::new(head_eq),
23878                    op: BinOp::And,
23879                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
23880                }),
23881            }
23882        }
23883        let negated_in = if matches!(self.peek(), Token::Not)
23884            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
23885        {
23886            self.advance();
23887            true
23888        } else {
23889            false
23890        };
23891        if matches!(self.peek(), Token::In) {
23892            self.advance();
23893            if !matches!(self.peek(), Token::LParen) {
23894                return Err(self.err(alloc::format!(
23895                    "expected '(' after row IN, got {:?}",
23896                    self.peek()
23897                )));
23898            }
23899            self.advance();
23900            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
23901            // not a list of literal rows. Row-vs-list decomposes to
23902            // OR-of-AND above, but the subquery's rows are only known at
23903            // runtime, so keep it as a RowInSubquery node.
23904            if matches!(self.peek(), Token::Select) {
23905                let inner = self.parse_select_stmt()?;
23906                if !matches!(self.peek(), Token::RParen) {
23907                    return Err(self.err(alloc::format!(
23908                        "expected ')' after row IN-subquery, got {:?}",
23909                        self.peek()
23910                    )));
23911                }
23912                self.advance();
23913                let Statement::Select(s) = inner else {
23914                    unreachable!("parse_select_stmt always returns Statement::Select")
23915                };
23916                return Ok(Expr::RowInSubquery {
23917                    row,
23918                    subquery: Box::new(s),
23919                    negated: negated_in,
23920                });
23921            }
23922            let mut alternatives: Vec<Expr> = Vec::new();
23923            loop {
23924                // Optional ROW keyword before the paren row.
23925                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23926                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23927                {
23928                    self.advance();
23929                }
23930                if !matches!(self.peek(), Token::LParen) {
23931                    return Err(self.err(alloc::format!(
23932                        "expected '(' to open a row inside IN, got {:?}",
23933                        self.peek()
23934                    )));
23935                }
23936                self.advance();
23937                let mut rhs = alloc::vec![self.parse_expr(0)?];
23938                while matches!(self.peek(), Token::Comma) {
23939                    self.advance();
23940                    rhs.push(self.parse_expr(0)?);
23941                }
23942                if !matches!(self.peek(), Token::RParen) {
23943                    return Err(self.err(alloc::format!(
23944                        "expected ')' after row inside IN, got {:?}",
23945                        self.peek()
23946                    )));
23947                }
23948                self.advance();
23949                if rhs.len() != row.len() {
23950                    return Err(self.err(alloc::format!(
23951                        "row IN arity mismatch: left has {}, right has {}",
23952                        row.len(),
23953                        rhs.len()
23954                    )));
23955                }
23956                alternatives.push(row_eq(&row, &rhs));
23957                if matches!(self.peek(), Token::Comma) {
23958                    self.advance();
23959                    continue;
23960                }
23961                break;
23962            }
23963            if !matches!(self.peek(), Token::RParen) {
23964                return Err(self.err(alloc::format!(
23965                    "expected ')' to close row IN list, got {:?}",
23966                    self.peek()
23967                )));
23968            }
23969            self.advance();
23970            let mut it = alternatives.into_iter();
23971            let first = it.next().expect("IN list has at least one row");
23972            let combined = it.fold(first, |acc, e| Expr::Binary {
23973                lhs: Box::new(acc),
23974                op: BinOp::Or,
23975                rhs: Box::new(e),
23976            });
23977            return Ok(maybe_not(combined, negated_in));
23978        }
23979        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23980        // two periods share at least one time point. Each pair is
23981        // normalised with least/greatest (PG accepts the endpoints
23982        // in either order), then lowered to the standard
23983        // `start1 < end2 AND start2 < end1` form.
23984        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23985            if row.len() != 2 {
23986                return Err(self.err(alloc::format!(
23987                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
23988                    row.len()
23989                )));
23990            }
23991            self.advance();
23992            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23993                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23994            {
23995                self.advance();
23996            }
23997            if !matches!(self.peek(), Token::LParen) {
23998                return Err(self.err(alloc::format!(
23999                    "expected '(' after OVERLAPS, got {:?}",
24000                    self.peek()
24001                )));
24002            }
24003            self.advance();
24004            let r0 = self.parse_expr(0)?;
24005            if !matches!(self.peek(), Token::Comma) {
24006                return Err(self.err(alloc::format!(
24007                    "OVERLAPS needs (start, end) on the right, got {:?}",
24008                    self.peek()
24009                )));
24010            }
24011            self.advance();
24012            let r1 = self.parse_expr(0)?;
24013            if !matches!(self.peek(), Token::RParen) {
24014                return Err(self.err(alloc::format!(
24015                    "expected ')' after OVERLAPS pair, got {:?}",
24016                    self.peek()
24017                )));
24018            }
24019            self.advance();
24020            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
24021                name: String::from(name),
24022                args: alloc::vec![a.clone(), b.clone()],
24023            };
24024            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
24025                lhs: Box::new(lhs),
24026                op: BinOp::Lt,
24027                rhs: Box::new(rhs),
24028            };
24029            return Ok(Expr::Binary {
24030                lhs: Box::new(lt(
24031                    pair_fn("least", &row[0], &row[1]),
24032                    pair_fn("greatest", &r0, &r1),
24033                )),
24034                op: BinOp::And,
24035                rhs: Box::new(lt(
24036                    pair_fn("least", &r0, &r1),
24037                    pair_fn("greatest", &row[0], &row[1]),
24038                )),
24039            });
24040        }
24041        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
24042        // PG, `IS NULL` is true only when EVERY field is NULL, and
24043        // `IS NOT NULL` is true only when every field is non-NULL — the
24044        // latter is NOT the negation of the former (a mixed row is
24045        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
24046        // which reproduces exactly that all-fields semantics.
24047        if matches!(self.peek(), Token::Is) {
24048            self.advance();
24049            let negated = if matches!(self.peek(), Token::Not) {
24050                self.advance();
24051                true
24052            } else {
24053                false
24054            };
24055            if !matches!(self.peek(), Token::Null) {
24056                return Err(self.err(alloc::format!(
24057                    "expected NULL after row IS [NOT], got {:?}",
24058                    self.peek()
24059                )));
24060            }
24061            self.advance();
24062            let mut it = row.iter().map(|e| Expr::IsNull {
24063                expr: Box::new(e.clone()),
24064                negated,
24065            });
24066            let first = it.next().expect("row has at least two elements");
24067            return Ok(it.fold(first, |acc, e| Expr::Binary {
24068                lhs: Box::new(acc),
24069                op: BinOp::And,
24070                rhs: Box::new(e),
24071            }));
24072        }
24073        let op = match self.peek() {
24074            Token::Eq => BinOp::Eq,
24075            Token::NotEq => BinOp::NotEq,
24076            Token::Lt => BinOp::Lt,
24077            Token::LtEq => BinOp::LtEq,
24078            Token::Gt => BinOp::Gt,
24079            Token::GtEq => BinOp::GtEq,
24080            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
24081            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
24082            // constructor value, identical to the `ROW(a, b, …)` keyword form:
24083            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
24084            // (`::text`, `.field`) applies at the caller just as it does for the
24085            // ROW(...) node. All the comparison / predicate forms returned above.
24086            _ => {
24087                return Ok(Expr::FunctionCall {
24088                    name: String::from("row"),
24089                    args: row,
24090                });
24091            }
24092        };
24093        self.advance();
24094        // Optional ROW keyword before the paren row.
24095        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
24096            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
24097        {
24098            self.advance();
24099        }
24100        if !matches!(self.peek(), Token::LParen) {
24101            return Err(self.err(alloc::format!(
24102                "expected '(' to open the right-hand row, got {:?}",
24103                self.peek()
24104            )));
24105        }
24106        self.advance();
24107        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
24108        // subquery. Kept as a node (the subquery's row is a runtime value);
24109        // the literal-RHS form below still decomposes at parse time.
24110        if matches!(self.peek(), Token::Select) {
24111            let inner = self.parse_select_stmt()?;
24112            if !matches!(self.peek(), Token::RParen) {
24113                return Err(self.err(alloc::format!(
24114                    "expected ')' after row comparison subquery, got {:?}",
24115                    self.peek()
24116                )));
24117            }
24118            self.advance();
24119            let Statement::Select(s) = inner else {
24120                unreachable!("parse_select_stmt always returns Statement::Select")
24121            };
24122            return Ok(Expr::RowCmpSubquery {
24123                row,
24124                op,
24125                subquery: Box::new(s),
24126            });
24127        }
24128        let mut rhs = alloc::vec![self.parse_expr(0)?];
24129        while matches!(self.peek(), Token::Comma) {
24130            self.advance();
24131            rhs.push(self.parse_expr(0)?);
24132        }
24133        if !matches!(self.peek(), Token::RParen) {
24134            return Err(self.err(alloc::format!(
24135                "expected ')' after right-hand row, got {:?}",
24136                self.peek()
24137            )));
24138        }
24139        self.advance();
24140        if rhs.len() != row.len() {
24141            // v7.39 (round 239) — PG's wording (42601).
24142            return Err(self.err("unequal number of entries in row expressions".to_string()));
24143        }
24144        Ok(match op {
24145            BinOp::Eq => row_eq(&row, &rhs),
24146            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
24147            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
24148            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
24149            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
24150            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
24151            _ => unreachable!("op restricted above"),
24152        })
24153    }
24154
24155    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
24156    /// escape character becomes the matcher's default backslash:
24157    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
24158    /// → the char itself, and any pre-existing backslash escapes
24159    /// itself so it stays literal. Both operands must be string
24160    /// literals — a runtime pattern would need matcher support.
24161    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
24162        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
24163            (&pattern, &esc)
24164        else {
24165            return Err(
24166                "LIKE ... ESCAPE requires string-literal pattern and escape \
24167                 (runtime escape characters are not supported yet)"
24168                    .into(),
24169            );
24170        };
24171        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
24172        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
24173        // multi-character escape is an error.
24174        let esc_ch: Option<char> = {
24175            let mut ch_iter = e.chars();
24176            match (ch_iter.next(), ch_iter.next()) {
24177                (Some(c), None) => Some(c),
24178                (None, _) => None,
24179                (Some(_), Some(_)) => {
24180                    return Err(alloc::format!(
24181                        "ESCAPE must be a single character, got {e:?}"
24182                    ));
24183                }
24184            }
24185        };
24186        let mut out = String::with_capacity(p.len() + 4);
24187        let mut chars = p.chars();
24188        while let Some(c) = chars.next() {
24189            if Some(c) == esc_ch {
24190                match chars.next() {
24191                    // Escaped wildcard / escaped escape → keep the
24192                    // next char literal via backslash.
24193                    Some(next) => {
24194                        out.push('\\');
24195                        out.push(next);
24196                    }
24197                    None => {
24198                        return Err("LIKE pattern ends with the escape character".into());
24199                    }
24200                }
24201            } else if c == '\\' && esc_ch != Some('\\') {
24202                // A raw backslash is literal under a custom (or absent) escape
24203                // — escape it for the backslash-based matcher.
24204                out.push('\\');
24205                out.push('\\');
24206            } else {
24207                out.push(c);
24208            }
24209        }
24210        Ok(Expr::Literal(Literal::String(out)))
24211    }
24212
24213    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
24214    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
24215    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
24216    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
24217    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
24218    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
24219    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
24220    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
24221    /// array expression errors honestly rather than silently mismatching.
24222    fn try_like_any_all(
24223        &mut self,
24224        base: &Expr,
24225        negated: bool,
24226        case_insensitive: bool,
24227    ) -> Result<Option<Expr>, ParseError> {
24228        let is_any = match self.peek() {
24229            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
24230            Token::Ident(s)
24231                if s.eq_ignore_ascii_case("any")
24232                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
24233            {
24234                true
24235            }
24236            _ => return Ok(None),
24237        };
24238        self.advance(); // ANY / ALL
24239        self.advance(); // '('
24240        let arr = self.parse_expr(0)?;
24241        if !matches!(self.peek(), Token::RParen) {
24242            return Err(self.err(format!(
24243                "expected ')' after LIKE {} argument, got {:?}",
24244                if is_any { "ANY" } else { "ALL" },
24245                self.peek()
24246            )));
24247        }
24248        self.advance(); // ')'
24249        let Expr::Array(items) = arr else {
24250            return Err(self.err(
24251                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
24252            ));
24253        };
24254        let mut clauses = items.into_iter().map(|p| Expr::Like {
24255            expr: Box::new(base.clone()),
24256            pattern: Box::new(p),
24257            negated,
24258            case_insensitive,
24259        });
24260        let Some(first) = clauses.next() else {
24261            // ANY(empty) = FALSE, ALL(empty) = TRUE.
24262            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
24263        };
24264        let op = if is_any { BinOp::Or } else { BinOp::And };
24265        let combined = clauses.fold(first, |acc, c| Expr::Binary {
24266            lhs: Box::new(acc),
24267            op,
24268            rhs: Box::new(c),
24269        });
24270        Ok(Some(combined))
24271    }
24272
24273    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
24274    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
24275    /// `AND` is not swallowed.
24276    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24277        self.advance(); // BETWEEN
24278        // SYMMETRIC — the bounds may arrive in either order; both
24279        // orientations OR together. ASYMMETRIC is the default and
24280        // absorbs as noise.
24281        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
24282        {
24283            self.advance();
24284            true
24285        } else {
24286            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
24287                self.advance();
24288            }
24289            false
24290        };
24291        let low = self.parse_expr(6)?;
24292        if !matches!(self.peek(), Token::And) {
24293            return Err(self.err(format!(
24294                "expected AND after BETWEEN low bound, got {:?}",
24295                self.peek()
24296            )));
24297        }
24298        self.advance();
24299        let high = self.parse_expr(6)?;
24300        let target = Box::new(expr);
24301        let range = |lo: Expr, hi: Expr| Expr::Binary {
24302            lhs: Box::new(Expr::Binary {
24303                lhs: target.clone(),
24304                op: BinOp::GtEq,
24305                rhs: Box::new(lo),
24306            }),
24307            op: BinOp::And,
24308            rhs: Box::new(Expr::Binary {
24309                lhs: target.clone(),
24310                op: BinOp::LtEq,
24311                rhs: Box::new(hi),
24312            }),
24313        };
24314        let combined = if symmetric {
24315            Expr::Binary {
24316                lhs: Box::new(range(low.clone(), high.clone())),
24317                op: BinOp::Or,
24318                rhs: Box::new(range(high, low)),
24319            }
24320        } else {
24321            range(low, high)
24322        };
24323        Ok(maybe_not(combined, negated))
24324    }
24325
24326    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
24327    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
24328    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
24329    /// Caller already consumed the leading `WITH` ident.
24330    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
24331    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
24332    /// self-reference that appears more than once in a single term.
24333    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
24334        use crate::ast::{CteBody, SelectStatement};
24335        if !cte.recursive {
24336            return Ok(());
24337        }
24338        let CteBody::Select(body) = &cte.body else {
24339            return Ok(());
24340        };
24341        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
24342        // check the anchor and every peer term.
24343        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
24344        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
24345        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
24346            return Err(self.err(String::from(
24347                "ORDER BY in a recursive query is not implemented",
24348            )));
24349        }
24350        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
24351            return Err(self.err(String::from(
24352                "LIMIT in a recursive query is not implemented",
24353            )));
24354        }
24355        let self_refs = |s: &SelectStatement| -> usize {
24356            let Some(from) = &s.from else {
24357                return 0;
24358            };
24359            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
24360            for j in &from.joins {
24361                if j.table.name.eq_ignore_ascii_case(&cte.name) {
24362                    n += 1;
24363                }
24364            }
24365            n
24366        };
24367        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
24368            return Err(self.err(alloc::format!(
24369                "recursive reference to query \"{}\" must not appear more than once",
24370                cte.name
24371            )));
24372        }
24373        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
24374        // apply only when the body actually references itself (a non-self-
24375        // referencing CTE under WITH RECURSIVE may use any set-op shape).
24376        let anchor_refs = self_refs(body);
24377        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
24378        if anchor_refs > 0 || union_refs {
24379            // Shape: the top level must be UNION [ALL] arms only. A self-ref
24380            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
24381            // "does not have the form" error — SPG used to compute a value.
24382            if body.unions.is_empty()
24383                || body.unions.iter().any(|(k, _)| {
24384                    !matches!(
24385                        k,
24386                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
24387                    )
24388                })
24389            {
24390                return Err(self.err(alloc::format!(
24391                    "recursive query \"{}\" does not have the form non-recursive-term \
24392                     UNION [ALL] recursive-term",
24393                    cte.name
24394                )));
24395            }
24396            if anchor_refs > 0 {
24397                return Err(self.err(alloc::format!(
24398                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
24399                    cte.name
24400                )));
24401            }
24402        }
24403        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
24404        for (_, u) in &body.unions {
24405            if self_refs(u) == 0 {
24406                continue;
24407            }
24408            // The self-reference must not sit on the nullable side of an outer
24409            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
24410            if let Some(from) = &u.from {
24411                for (i, j) in from.joins.iter().enumerate() {
24412                    let left_has_self = is_self(&from.primary)
24413                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
24414                    let violated = match j.kind {
24415                        crate::ast::JoinKind::Left => is_self(&j.table),
24416                        crate::ast::JoinKind::Right => left_has_self,
24417                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
24418                        _ => false,
24419                    };
24420                    if violated {
24421                        return Err(self.err(alloc::format!(
24422                            "recursive reference to query \"{}\" must not appear within an outer join",
24423                            cte.name
24424                        )));
24425                    }
24426                }
24427            }
24428            // No aggregates at the top level of the recursive term (SPG used
24429            // to run them and surface a misleading downstream error).
24430            let mut items_and_having: Vec<&Expr> = Vec::new();
24431            for it in &u.items {
24432                if let crate::ast::SelectItem::Expr { expr, .. } = it {
24433                    items_and_having.push(expr);
24434                }
24435            }
24436            if let Some(h) = &u.having {
24437                items_and_having.push(h);
24438            }
24439            for e in items_and_having {
24440                if expr_has_toplevel_aggregate(e) {
24441                    return Err(self.err(String::from(
24442                        "aggregate functions are not allowed in a recursive query's recursive term",
24443                    )));
24444                }
24445            }
24446        }
24447        // A self-reference inside a sublink expression (EXISTS / IN / scalar
24448        // subquery) anywhere in the body is rejected; a plain FROM derived
24449        // table is legal in PG and untouched here.
24450        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
24451        all_terms.extend(body.unions.iter().map(|(_, u)| u));
24452        for term in all_terms {
24453            if select_has_self_ref_in_sublink(term, &cte.name) {
24454                return Err(self.err(alloc::format!(
24455                    "recursive reference to query \"{}\" must not appear within a subquery",
24456                    cte.name
24457                )));
24458            }
24459        }
24460        Ok(())
24461    }
24462
24463    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
24464    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
24465    /// right after parse so the engine sees a plain recursive CTE with the
24466    /// tracking columns already projected. DEPTH FIRST and CYCLE are
24467    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
24468    /// text-rendered rows can't provide, and errors honestly.
24469    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
24470        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
24471        if cte.search.is_none() && cte.cycle.is_none() {
24472            return Ok(());
24473        }
24474        let cte_name = cte.name.clone();
24475        let col_names = cte.column_overrides.clone();
24476        if col_names.is_empty() {
24477            return Err(
24478                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
24479            );
24480        }
24481        let search = cte.search.take();
24482        let cycle = cte.cycle.take();
24483        let mut extra_cols: Vec<String> = Vec::new();
24484        let col_ref = |name: &str| {
24485            Expr::Column(ColumnName {
24486                qualifier: Some(cte_name.clone()),
24487                name: name.to_string(),
24488            })
24489        };
24490        // Position of a SEARCH/CYCLE column within the CTE's column list.
24491        let pos_of = |name: &str| -> Result<usize, ParseError> {
24492            col_names
24493                .iter()
24494                .position(|c| c.eq_ignore_ascii_case(name))
24495                .ok_or_else(|| {
24496                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
24497                })
24498        };
24499        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
24500            let mut args = Vec::with_capacity(positions.len());
24501            for &p in positions {
24502                match items.get(p) {
24503                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
24504                    _ => {
24505                        return Err(self.err(
24506                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
24507                        ));
24508                    }
24509                }
24510            }
24511            Ok(Expr::FunctionCall {
24512                name: "row".into(),
24513                args,
24514            })
24515        };
24516        let CteBody::Select(body) = &mut cte.body else {
24517            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
24518        };
24519        if body.unions.is_empty() {
24520            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
24521        }
24522        let rec = body.unions.len() - 1; // recursive term = last UNION peer
24523
24524        if let Some(srch) = search {
24525            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
24526            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
24527            // no typed `record[]`, but element-wise array ORDER BY is correct
24528            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
24529            // exactly onto a typed array: DEPTH is the root→node path
24530            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
24531            // orders numerically (multi-digit keys included), matching PG.
24532            //
24533            // A multi-column BY would need a record[] to keep the per-node key
24534            // tuple orderable, which SPG can't express — error honestly there
24535            // rather than mis-order.
24536            if srch.by_columns.len() != 1 {
24537                return Err(self.err(
24538                    "SEARCH … BY with multiple columns needs typed record[] ordering \
24539                     SPG doesn't have yet; a single BY column is supported"
24540                        .into(),
24541                ));
24542            }
24543            let key_pos = pos_of(&srch.by_columns[0])?;
24544            let base_key = match body.items.get(key_pos) {
24545                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24546                _ => {
24547                    return Err(
24548                        self.err("SEARCH BY column maps to a non-expression select item".into())
24549                    );
24550                }
24551            };
24552            let rec_key = match body.unions[rec].1.items.get(key_pos) {
24553                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24554                _ => {
24555                    return Err(
24556                        self.err("SEARCH BY column maps to a non-expression select item".into())
24557                    );
24558                }
24559            };
24560            if srch.depth_first {
24561                // base: ARRAY[key]; rec: array_append(cte.set, key).
24562                body.items.push(SelectItem::Expr {
24563                    expr: Expr::Array(alloc::vec![base_key]),
24564                    alias: Some(srch.set_column.clone()),
24565                });
24566                body.unions[rec].1.items.push(SelectItem::Expr {
24567                    expr: Expr::FunctionCall {
24568                        name: "array_append".into(),
24569                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
24570                    },
24571                    alias: Some(srch.set_column.clone()),
24572                });
24573            } else {
24574                // BREADTH: [depth, key]; depth starts at 0 and increments. The
24575                // leading depth element dominates the element-wise comparison,
24576                // so shallower rows sort first, then by key — PG's (depth, key).
24577                body.items.push(SelectItem::Expr {
24578                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
24579                    alias: Some(srch.set_column.clone()),
24580                });
24581                // rec depth = cte.set[1] + 1.
24582                let parent_depth = Expr::ArraySubscript {
24583                    target: Box::new(col_ref(&srch.set_column)),
24584                    index: Box::new(Expr::Literal(Literal::Integer(1))),
24585                };
24586                body.unions[rec].1.items.push(SelectItem::Expr {
24587                    expr: Expr::Array(alloc::vec![
24588                        Expr::Binary {
24589                            lhs: Box::new(parent_depth),
24590                            op: BinOp::Add,
24591                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
24592                        },
24593                        rec_key,
24594                    ]),
24595                    alias: Some(srch.set_column.clone()),
24596                });
24597            }
24598            extra_cols.push(srch.set_column);
24599        }
24600
24601        if let Some(cyc) = cycle {
24602            let positions: Vec<usize> = cyc
24603                .columns
24604                .iter()
24605                .map(|c| pos_of(c))
24606                .collect::<Result<_, _>>()?;
24607            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
24608            // cast it to text for the cycle path: membership only needs equality,
24609            // and the record text form gives SPG a TextArray path (SPG has no
24610            // typed record[] array). Cycle detection is unaffected.
24611            let base_row = Expr::Cast {
24612                expr: Box::new(row_of(&body.items, &positions)?),
24613                target: CastTarget::Text,
24614            };
24615            let rec_row = Expr::Cast {
24616                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
24617                target: CastTarget::Text,
24618            };
24619            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
24620            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
24621            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
24622            body.items.push(SelectItem::Expr {
24623                expr: Expr::Literal(dflt.clone()),
24624                alias: Some(cyc.mark_column.clone()),
24625            });
24626            body.items.push(SelectItem::Expr {
24627                expr: Expr::Array(alloc::vec![base_row]),
24628                alias: Some(cyc.path_column.clone()),
24629            });
24630            // rec mark: ROW(cols) already in the path → cycle.
24631            let hit = Expr::AnyAll {
24632                expr: Box::new(rec_row.clone()),
24633                op: BinOp::Eq,
24634                array: Box::new(col_ref(&cyc.path_column)),
24635                is_any: true,
24636            };
24637            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
24638                Expr::Case {
24639                    operand: None,
24640                    branches: alloc::vec![(hit, Expr::Literal(mark))],
24641                    else_branch: Some(Box::new(Expr::Literal(dflt))),
24642                }
24643            } else {
24644                hit
24645            };
24646            body.unions[rec].1.items.push(SelectItem::Expr {
24647                expr: mark_expr,
24648                alias: Some(cyc.mark_column.clone()),
24649            });
24650            // rec path: array_append(cte.path, ROW(cols)).
24651            body.unions[rec].1.items.push(SelectItem::Expr {
24652                expr: Expr::FunctionCall {
24653                    name: "array_append".into(),
24654                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
24655                },
24656                alias: Some(cyc.path_column.clone()),
24657            });
24658            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
24659            let stop = Expr::Unary {
24660                op: UnOp::Not,
24661                expr: Box::new(col_ref(&cyc.mark_column)),
24662            };
24663            let w = &mut body.unions[rec].1.where_;
24664            *w = Some(match w.take() {
24665                Some(prev) => Expr::Binary {
24666                    lhs: Box::new(prev),
24667                    op: BinOp::And,
24668                    rhs: Box::new(stop),
24669                },
24670                None => stop,
24671            });
24672            extra_cols.push(cyc.mark_column);
24673            extra_cols.push(cyc.path_column);
24674        }
24675        cte.column_overrides.extend(extra_cols);
24676        Ok(())
24677    }
24678
24679    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
24680    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
24681    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
24682        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
24683            return Ok(None);
24684        }
24685        self.advance(); // SEARCH
24686        let depth_first = match self.peek() {
24687            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
24688            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
24689            other => {
24690                return Err(self.err(format!(
24691                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
24692                )));
24693            }
24694        };
24695        self.advance();
24696        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
24697            return Err(self.err(format!(
24698                "expected FIRST after SEARCH mode, got {:?}",
24699                self.peek()
24700            )));
24701        }
24702        self.advance();
24703        if !self.peek_is_by() {
24704            return Err(self.err(format!(
24705                "expected BY after SEARCH … FIRST, got {:?}",
24706                self.peek()
24707            )));
24708        }
24709        self.advance();
24710        let mut by_columns = alloc::vec![self.expect_ident_like()?];
24711        while matches!(self.peek(), Token::Comma) {
24712            self.advance();
24713            by_columns.push(self.expect_ident_like()?);
24714        }
24715        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24716            return Err(self.err(format!(
24717                "expected SET in SEARCH clause, got {:?}",
24718                self.peek()
24719            )));
24720        }
24721        self.advance();
24722        let set_column = self.expect_ident_like()?;
24723        Ok(Some(crate::ast::SearchClause {
24724            depth_first,
24725            by_columns,
24726            set_column,
24727        }))
24728    }
24729
24730    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
24731    /// USING pathcol`. Returns None when the next token isn't CYCLE.
24732    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
24733        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
24734            return Ok(None);
24735        }
24736        self.advance(); // CYCLE
24737        let mut columns = alloc::vec![self.expect_ident_like()?];
24738        while matches!(self.peek(), Token::Comma) {
24739            self.advance();
24740            columns.push(self.expect_ident_like()?);
24741        }
24742        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24743            return Err(self.err(format!(
24744                "expected SET in CYCLE clause, got {:?}",
24745                self.peek()
24746            )));
24747        }
24748        self.advance();
24749        let mark_column = self.expect_ident_like()?;
24750        let mut mark_value = None;
24751        let mut default_value = None;
24752        if matches!(self.peek(), Token::To) {
24753            self.advance();
24754            mark_value = Some(self.parse_cycle_literal()?);
24755            if !matches!(self.peek(), Token::Default) {
24756                return Err(self.err(format!(
24757                    "expected DEFAULT after CYCLE … TO, got {:?}",
24758                    self.peek()
24759                )));
24760            }
24761            self.advance();
24762            default_value = Some(self.parse_cycle_literal()?);
24763        }
24764        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
24765            return Err(self.err(format!(
24766                "expected USING in CYCLE clause, got {:?}",
24767                self.peek()
24768            )));
24769        }
24770        self.advance();
24771        let path_column = self.expect_ident_like()?;
24772        Ok(Some(crate::ast::CycleClause {
24773            columns,
24774            mark_column,
24775            mark_value,
24776            default_value,
24777            path_column,
24778        }))
24779    }
24780
24781    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
24782    /// literal (string / bool / number) in PG.
24783    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
24784        match self.parse_expr(0)? {
24785            Expr::Literal(l) => Ok(l),
24786            other => Err(self.err(format!(
24787                "CYCLE mark/default value must be a literal, got {other:?}"
24788            ))),
24789        }
24790    }
24791
24792    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
24793        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
24794        // Comes through as an identifier; consume it if present and
24795        // mark every CTE in the clause as recursive (PG semantics —
24796        // the flag is per-WITH, not per-CTE).
24797        let mut recursive = false;
24798        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24799            && s.eq_ignore_ascii_case("recursive")
24800        {
24801            self.advance();
24802            recursive = true;
24803        }
24804        let mut ctes = Vec::new();
24805        loop {
24806            let name = self.expect_ident_like()?;
24807            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
24808            // PG uses these to rename the body's output columns; we
24809            // do the same below by overriding `columns[i].name`.
24810            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
24811                self.advance();
24812                let mut names = Vec::new();
24813                loop {
24814                    names.push(self.expect_ident_like()?);
24815                    if matches!(self.peek(), Token::Comma) {
24816                        self.advance();
24817                        continue;
24818                    }
24819                    break;
24820                }
24821                if !matches!(self.peek(), Token::RParen) {
24822                    return Err(self.err(format!(
24823                        "expected ')' to close CTE column list, got {:?}",
24824                        self.peek()
24825                    )));
24826                }
24827                self.advance();
24828                names
24829            } else {
24830                Vec::new()
24831            };
24832            // AS is a reserved Token::As (used by SELECT-item / FROM
24833            // aliasing) — handle it specially rather than as a bare
24834            // ident.
24835            if !matches!(self.peek(), Token::As) {
24836                return Err(self.err(format!(
24837                    "expected AS after CTE name {name:?}, got {:?}",
24838                    self.peek()
24839                )));
24840            }
24841            self.advance();
24842            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
24843            // MATERIALIZED` optimizer hints. SPG materialises every
24844            // CTE, so both spellings are accepted and absorbed.
24845            if matches!(self.peek(), Token::Not) {
24846                self.advance(); // NOT
24847                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24848                    if s.eq_ignore_ascii_case("materialized"))
24849                {
24850                    self.advance();
24851                } else {
24852                    return Err(self.err(format!(
24853                        "expected MATERIALIZED after AS NOT, got {:?}",
24854                        self.peek()
24855                    )));
24856                }
24857            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24858                if s.eq_ignore_ascii_case("materialized"))
24859            {
24860                self.advance();
24861            }
24862            if !matches!(self.peek(), Token::LParen) {
24863                return Err(self.err(format!(
24864                    "expected '(' after AS in WITH clause, got {:?}",
24865                    self.peek()
24866                )));
24867            }
24868            self.advance();
24869            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
24870            // RETURNING) as the CTE body in addition to SELECT.
24871            // PG writable CTE semantics. UPDATE / DELETE come in as
24872            // bare Idents (lexer keeps SELECT / INSERT as reserved
24873            // tokens but treats the rest of DML as case-insensitive
24874            // idents).
24875            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24876            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24877            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24878            let body = match self.peek() {
24879                Token::Select => {
24880                    let inner = self.parse_select_stmt()?;
24881                    let Statement::Select(s) = inner else {
24882                        unreachable!("parse_select_stmt returns Select");
24883                    };
24884                    crate::ast::CteBody::Select(s)
24885                }
24886                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24887                // `SELECT * FROM t` this way and accepts it wherever a
24888                // SELECT goes, so the CTE body dispatch needs its own
24889                // arm: this match is keyed on the FIRST token, and
24890                // `Token::Table` fell through to a tail that then
24891                // rejected what it got. `parse_table_shorthand` has
24892                // returned a desugared SelectStatement since the
24893                // shorthand landed — only the routing was missing.
24894                // Round 868 found this by putting the shorthand in a
24895                // subquery; every earlier check used a top-level form.
24896                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24897                // `SELECT * FROM t` this way and accepts it wherever a
24898                // SELECT goes, so the CTE body dispatch needs its own
24899                // arm: this match is keyed on the FIRST token, and
24900                // `Token::Table` fell through to a tail that rejected
24901                // what it got. `parse_table_shorthand` has returned a
24902                // desugared SelectStatement since the shorthand landed —
24903                // only the routing was missing, here and in the derived
24904                // table's second-token gate. Round 868 found both by
24905                // putting the shorthand in a subquery; every earlier
24906                // check had used a top-level form.
24907                Token::Table
24908                    if matches!(
24909                        self.tokens.get(self.pos + 1),
24910                        Some(Token::Ident(_) | Token::QuotedIdent(_))
24911                    ) =>
24912                {
24913                    let mut head = self.parse_table_shorthand()?;
24914                    self.parse_setop_chain_into(&mut head)?;
24915                    self.parse_select_tail_into(&mut head)?;
24916                    crate::ast::CteBody::Select(head)
24917                }
24918                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
24919                // WITH t(a) AS (VALUES (1), (2)) … lowers through
24920                // the shared rows helper onto a Select body.
24921                Token::Values => {
24922                    self.advance(); // VALUES
24923                    let mut head = self.parse_values_rows_body()?;
24924                    // A VALUES seed can head a set-operation chain —
24925                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
24926                    // SELECT n+1 FROM t …). Attach any trailing
24927                    // UNION / INTERSECT / EXCEPT peers so the
24928                    // recursive-CTE body parses like the SELECT seed.
24929                    self.parse_setop_chain_into(&mut head)?;
24930                    crate::ast::CteBody::Select(head)
24931                }
24932                Token::Insert => {
24933                    let inner = self.parse_one_statement()?;
24934                    let Statement::Insert(s) = inner else {
24935                        unreachable!("Token::Insert routes to Insert");
24936                    };
24937                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24938                }
24939                _ if is_update_kw => {
24940                    let inner = self.parse_one_statement()?;
24941                    let Statement::Update(s) = inner else {
24942                        return Err(
24943                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
24944                        );
24945                    };
24946                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24947                }
24948                _ if is_delete_kw => {
24949                    let inner = self.parse_one_statement()?;
24950                    let Statement::Delete(s) = inner else {
24951                        return Err(
24952                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
24953                        );
24954                    };
24955                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24956                }
24957                // v7.39 (round 149) — PG 17 allows MERGE as a
24958                // data-modifying CTE body.
24959                _ if is_merge_kw => {
24960                    let inner = self.parse_one_statement()?;
24961                    let Statement::Merge(s) = inner else {
24962                        return Err(
24963                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
24964                        );
24965                    };
24966                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24967                }
24968                // v7.39 (round 151) — a CTE body may itself be
24969                // WITH-headed (PG grammar: PreparableStmt carries its
24970                // own with_clause). The nested statement keeps its own
24971                // ctes; the modifying-CTE-at-top-level rule is enforced
24972                // at execution.
24973                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24974                    self.advance(); // WITH
24975                    match self.parse_with_cte_then_select()? {
24976                        Statement::Select(s) => crate::ast::CteBody::Select(s),
24977                        Statement::Insert(s) => {
24978                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24979                        }
24980                        Statement::Update(s) => {
24981                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24982                        }
24983                        Statement::Delete(s) => {
24984                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24985                        }
24986                        Statement::Merge(s) => {
24987                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24988                        }
24989
24990                        other => {
24991                            return Err(self.err(format!(
24992                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24993                            )));
24994                        }
24995                    }
24996                }
24997                other => {
24998                    return Err(self.err(format!(
24999                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
25000                    )));
25001                }
25002            };
25003            if !matches!(self.peek(), Token::RParen) {
25004                return Err(self.err(format!(
25005                    "expected ')' after CTE body, got {:?}",
25006                    self.peek()
25007                )));
25008            }
25009            self.advance();
25010            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
25011            // CTE, desugared into extra body columns by the engine.
25012            let search = self.parse_cte_search_clause()?;
25013            let cycle = self.parse_cte_cycle_clause()?;
25014            let mut cte = crate::ast::Cte {
25015                name,
25016                body,
25017                recursive,
25018                column_overrides,
25019                search,
25020                cycle,
25021            };
25022            self.validate_recursive_cte(&cte)?;
25023            self.desugar_cte_search_cycle(&mut cte)?;
25024            ctes.push(cte);
25025            if matches!(self.peek(), Token::Comma) {
25026                self.advance();
25027                continue;
25028            }
25029            break;
25030        }
25031        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
25032        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
25033        // the parsed CTEs to whichever statement the body produces.
25034        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
25035        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
25036        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
25037        match self.peek() {
25038            Token::Select => {
25039                let body_stmt = self.parse_select_stmt()?;
25040                let Statement::Select(mut body) = body_stmt else {
25041                    unreachable!()
25042                };
25043                body.ctes = ctes;
25044                Ok(Statement::Select(body))
25045            }
25046            Token::Insert => {
25047                let body_stmt = self.parse_one_statement()?;
25048                let Statement::Insert(mut body) = body_stmt else {
25049                    unreachable!()
25050                };
25051                body.ctes = ctes;
25052                Ok(Statement::Insert(body))
25053            }
25054            _ if outer_is_update => {
25055                let body_stmt = self.parse_one_statement()?;
25056                let Statement::Update(mut body) = body_stmt else {
25057                    return Err(self.err(format!("expected UPDATE after WITH clause")));
25058                };
25059                body.ctes = ctes;
25060                Ok(Statement::Update(body))
25061            }
25062            _ if outer_is_delete => {
25063                let body_stmt = self.parse_one_statement()?;
25064                let Statement::Delete(mut body) = body_stmt else {
25065                    return Err(self.err(format!("expected DELETE after WITH clause")));
25066                };
25067                body.ctes = ctes;
25068                Ok(Statement::Delete(body))
25069            }
25070            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
25071            // WITH RECURSIVE is rejected with PG's exact message
25072            // (parse analysis, transformWithClause).
25073            _ if outer_is_merge => {
25074                if recursive {
25075                    return Err(self.err(String::from(
25076                        "WITH RECURSIVE is not supported for MERGE statement",
25077                    )));
25078                }
25079                let body_stmt = self.parse_one_statement()?;
25080                let Statement::Merge(mut body) = body_stmt else {
25081                    return Err(self.err(format!("expected MERGE after WITH clause")));
25082                };
25083                body.ctes = ctes;
25084                Ok(Statement::Merge(body))
25085            }
25086            other => Err(self.err(format!(
25087                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
25088            ))),
25089        }
25090    }
25091
25092    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
25093    /// already consumed the leading `EXISTS` ident via
25094    /// `self.advance()`.
25095    /// v7.13.0 — parse the rest of a `CASE … END` expression after
25096    /// the leading `CASE` ident has been consumed (mailrs round-5
25097    /// G9). Supports both the searched form
25098    /// (`CASE WHEN cond THEN val …`) and the simple form
25099    /// (`CASE operand WHEN val THEN val …`).
25100    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
25101        // Disambiguate searched vs simple form: if the next token
25102        // is `WHEN`, we're in the searched form. Otherwise the
25103        // intervening expression is the operand.
25104        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
25105            None
25106        } else {
25107            Some(Box::new(self.parse_expr(0)?))
25108        };
25109        let mut branches: Vec<(Expr, Expr)> = Vec::new();
25110        loop {
25111            match self.peek() {
25112                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
25113                    self.advance();
25114                    let cond = self.parse_expr(0)?;
25115                    match self.peek() {
25116                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
25117                            self.advance();
25118                        }
25119                        other => {
25120                            return Err(self.err(alloc::format!(
25121                                "expected THEN after CASE WHEN <expr>, got {other:?}"
25122                            )));
25123                        }
25124                    }
25125                    let value = self.parse_expr(0)?;
25126                    branches.push((cond, value));
25127                }
25128                _ => break,
25129            }
25130        }
25131        if branches.is_empty() {
25132            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
25133        }
25134        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
25135        {
25136            self.advance();
25137            Some(Box::new(self.parse_expr(0)?))
25138        } else {
25139            None
25140        };
25141        match self.peek() {
25142            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
25143                self.advance();
25144            }
25145            other => {
25146                return Err(self.err(alloc::format!(
25147                    "expected END to close CASE expression, got {other:?}"
25148                )));
25149            }
25150        }
25151        Ok(Expr::Case {
25152            operand,
25153            branches,
25154            else_branch,
25155        })
25156    }
25157
25158    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
25159    /// query-source position (EXISTS / IN / INSERT source / CTE body /
25160    /// view body). Caller consumed the WITH keyword. Only a SELECT
25161    /// outer is grammatical here; the data-modifying-CTE-at-top-level
25162    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
25163    /// maps correctly.
25164    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
25165        let inner = self.parse_with_cte_then_select()?;
25166        match inner {
25167            Statement::Select(s) => Ok(s),
25168            other => Err(self.err(format!(
25169                "expected SELECT after WITH in a subquery, got {other:?}"
25170            ))),
25171        }
25172    }
25173
25174    /// True when the next token is the (unquoted) WITH keyword. WITH is
25175    /// reserved in PG, so a bare `with` can never be a column reference
25176    /// in these positions; a quoted `"with"` stays an identifier.
25177    fn peek_is_with_kw(&self) -> bool {
25178        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
25179    }
25180
25181    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
25182    /// `#[inline(never)]` keeps the large SelectStatement temporaries
25183    /// off parse_expr's recursive frame (the nesting-budget stack
25184    /// cliff — see the round-153 gate regression).
25185    #[inline(never)]
25186    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
25187        if self.peek_is_with_kw() {
25188            self.advance();
25189            self.parse_nested_with_select()
25190        } else {
25191            match self.parse_select_stmt()? {
25192                Statement::Select(s) => Ok(s),
25193                other => Err(self.err(alloc::format!(
25194                    "expected SELECT inside ANY/ALL, got {other:?}"
25195                ))),
25196            }
25197        }
25198    }
25199
25200    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
25201        if !matches!(self.peek(), Token::LParen) {
25202            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
25203        }
25204        self.advance();
25205        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
25206        let s = if self.peek_is_with_kw() {
25207            self.advance();
25208            self.parse_nested_with_select()?
25209        } else {
25210            let inner = self.parse_select_stmt()?;
25211            let Statement::Select(s) = inner else {
25212                unreachable!("parse_select_stmt returns Select")
25213            };
25214            s
25215        };
25216        if !matches!(self.peek(), Token::RParen) {
25217            return Err(self.err(format!(
25218                "expected ')' after EXISTS-subquery, got {:?}",
25219                self.peek()
25220            )));
25221        }
25222        self.advance();
25223        Ok(Expr::Exists {
25224            subquery: Box::new(s),
25225            negated,
25226        })
25227    }
25228
25229    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
25230        self.advance(); // IN
25231        if !matches!(self.peek(), Token::LParen) {
25232            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
25233        }
25234        self.advance();
25235        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
25236        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
25237        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
25238            let s = if self.peek_is_with_kw() {
25239                self.advance();
25240                self.parse_nested_with_select()?
25241            } else {
25242                let inner = self.parse_select_stmt()?;
25243                let Statement::Select(s) = inner else {
25244                    unreachable!("parse_select_stmt always returns Statement::Select")
25245                };
25246                s
25247            };
25248            if !matches!(self.peek(), Token::RParen) {
25249                return Err(self.err(format!(
25250                    "expected ')' after IN-subquery, got {:?}",
25251                    self.peek()
25252                )));
25253            }
25254            self.advance();
25255            return Ok(Expr::InSubquery {
25256                expr: Box::new(expr),
25257                subquery: Box::new(s),
25258                negated,
25259            });
25260        }
25261        let mut elements = Vec::new();
25262        if !matches!(self.peek(), Token::RParen) {
25263            loop {
25264                elements.push(self.parse_expr(0)?);
25265                match self.peek() {
25266                    Token::Comma => {
25267                        self.advance();
25268                    }
25269                    Token::RParen => break,
25270                    other => {
25271                        return Err(
25272                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
25273                        );
25274                    }
25275                }
25276            }
25277        }
25278        self.advance(); // ')'
25279        // v7.30.2 (mailrs round-25) — flat InList node instead of a
25280        // left-deep OR-Eq chain: chain depth scaled with the element
25281        // count and overflowed the stack (eval + drop are recursive).
25282        if elements.is_empty() {
25283            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
25284        }
25285        Ok(Expr::InList {
25286            expr: Box::new(expr),
25287            list: elements,
25288            negated,
25289        })
25290    }
25291
25292    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
25293    /// already consumed by the caller. Elements must be numeric literals
25294    /// (with optional unary `-`); any compound expression is rejected at
25295    /// parse time so the runtime never needs to evaluate inside a vector.
25296    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
25297    /// has already consumed the `EXTRACT` token before calling us —
25298    /// we pick up at the opening `(`.
25299    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
25300    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
25301    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
25302    /// per-column OR-fold of
25303    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
25304    /// term)` so the existing FTS evaluator handles semantics.
25305    ///
25306    /// The mode modifier is accepted-and-ignored at v7.17 — all
25307    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
25308    /// mode operators (`+foo -bar`) would need their own parser
25309    /// (Phase 2.2c); customers who hit them today already get a
25310    /// correct lexeme-match against the bare term, only without
25311    /// the +/- precedence the customer asked for.
25312    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
25313        // Already at `MATCH`-consumed position; the dispatcher
25314        // confirmed the next token is `(`.
25315        if !matches!(self.peek(), Token::LParen) {
25316            return Err(self.err(alloc::format!(
25317                "expected '(' after MATCH, got {:?}",
25318                self.peek()
25319            )));
25320        }
25321        self.advance();
25322        let mut cols: Vec<Expr> = Vec::new();
25323        loop {
25324            cols.push(self.parse_expr(0)?);
25325            match self.peek() {
25326                Token::Comma => {
25327                    self.advance();
25328                }
25329                Token::RParen => break,
25330                other => {
25331                    return Err(self.err(alloc::format!(
25332                        "expected ',' or ')' in MATCH column list, got {other:?}"
25333                    )));
25334                }
25335            }
25336        }
25337        self.advance(); // ')'
25338        // Expect AGAINST.
25339        match self.peek() {
25340            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
25341                self.advance();
25342            }
25343            other => {
25344                return Err(self.err(alloc::format!(
25345                    "expected AGAINST after MATCH column list, got {other:?}"
25346                )));
25347            }
25348        }
25349        if !matches!(self.peek(), Token::LParen) {
25350            return Err(self.err(alloc::format!(
25351                "expected '(' after AGAINST, got {:?}",
25352                self.peek()
25353            )));
25354        }
25355        self.advance();
25356        // Read AGAINST's argument as a single primary token —
25357        // string literal, placeholder, or column-ref ident. We
25358        // can't call `parse_expr` / `parse_unary` here because
25359        // the postfix chain inside `parse_atom` would greedily
25360        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
25361        // and fail at "expected '(' after IN". Customers always
25362        // write a literal or bound parameter in AGAINST, so this
25363        // restriction is non-blocking; the error path explains
25364        // the limit if a more complex expression shows up.
25365        let term = match self.advance() {
25366            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
25367            Token::Placeholder(n) => Expr::Placeholder(n),
25368            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
25369                qualifier: None,
25370                name: s,
25371            }),
25372            other => {
25373                return Err(self.err(alloc::format!(
25374                    "MATCH ... AGAINST(<term>) expects a string literal, \
25375                     bound parameter, or column ref, got {other:?}"
25376                )));
25377            }
25378        };
25379        // Optional mode tail — accept-and-ignore at v7.17:
25380        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
25381        //   IN BOOLEAN MODE
25382        //   WITH QUERY EXPANSION
25383        loop {
25384            match self.peek() {
25385                // IN lexes as a reserved Token::In, not an ident,
25386                // so it gets its own arm.
25387                Token::In => {
25388                    self.advance();
25389                }
25390                Token::Ident(s) | Token::QuotedIdent(s)
25391                    if s.eq_ignore_ascii_case("natural")
25392                        || s.eq_ignore_ascii_case("language")
25393                        || s.eq_ignore_ascii_case("boolean")
25394                        || s.eq_ignore_ascii_case("mode")
25395                        || s.eq_ignore_ascii_case("with")
25396                        || s.eq_ignore_ascii_case("query")
25397                        || s.eq_ignore_ascii_case("expansion") =>
25398                {
25399                    self.advance();
25400                }
25401                _ => break,
25402            }
25403        }
25404        if !matches!(self.peek(), Token::RParen) {
25405            return Err(self.err(alloc::format!(
25406                "expected ')' to close AGAINST, got {:?}",
25407                self.peek()
25408            )));
25409        }
25410        self.advance();
25411        // Build per-column `to_tsvector('simple', col) @@
25412        // plainto_tsquery('simple', term)` and OR-fold.
25413        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
25414        let plainto = Expr::FunctionCall {
25415            name: String::from("plainto_tsquery"),
25416            args: alloc::vec![simple_lit(), term.clone()],
25417        };
25418        let mut folded: Option<Expr> = None;
25419        for col in cols {
25420            let to_tsv = Expr::FunctionCall {
25421                name: String::from("to_tsvector"),
25422                args: alloc::vec![simple_lit(), col],
25423            };
25424            let leaf = Expr::Binary {
25425                lhs: Box::new(to_tsv),
25426                op: crate::ast::BinOp::TsMatch,
25427                rhs: Box::new(plainto.clone()),
25428            };
25429            folded = Some(match folded {
25430                None => leaf,
25431                Some(prev) => Expr::Binary {
25432                    lhs: Box::new(prev),
25433                    op: crate::ast::BinOp::Or,
25434                    rhs: Box::new(leaf),
25435                },
25436            });
25437        }
25438        match folded {
25439            Some(e) => Ok(e),
25440            None => Err(self.err(String::from(
25441                "MATCH(...) AGAINST(...) requires at least one column",
25442            ))),
25443        }
25444    }
25445
25446    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
25447        if !matches!(self.peek(), Token::LParen) {
25448            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
25449        }
25450        self.advance();
25451        let field_name = self.expect_ident_like()?;
25452        let field = match field_name.to_ascii_lowercase().as_str() {
25453            // PG accepts the plural spellings (years/months/…/millenniums) as
25454            // aliases for the singular fields — its datetime unit table has both.
25455            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
25456            "year" | "years" => ExtractField::Year,
25457            "month" | "months" => ExtractField::Month,
25458            "day" | "days" => ExtractField::Day,
25459            "hour" | "hours" => ExtractField::Hour,
25460            "minute" | "minutes" => ExtractField::Minute,
25461            "second" | "seconds" => ExtractField::Second,
25462            "microsecond" | "microseconds" => ExtractField::Microsecond,
25463            "epoch" => ExtractField::Epoch,
25464            "dow" => ExtractField::Dow,
25465            "isodow" => ExtractField::Isodow,
25466            "doy" => ExtractField::Doy,
25467            "week" | "weeks" => ExtractField::Week,
25468            "isoyear" => ExtractField::Isoyear,
25469            "quarter" => ExtractField::Quarter,
25470            "decade" | "decades" => ExtractField::Decade,
25471            "century" | "centuries" => ExtractField::Century,
25472            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
25473            "julian" => ExtractField::Julian,
25474            "millisecond" | "milliseconds" => ExtractField::Millisecond,
25475            "timezone" => ExtractField::Timezone,
25476            "timezone_hour" => ExtractField::TimezoneHour,
25477            "timezone_minute" => ExtractField::TimezoneMinute,
25478            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
25479            // reports an unknown one with the source type (22023); carry the
25480            // raw name so eval can word it.
25481            other => ExtractField::Other(alloc::string::String::from(other)),
25482        };
25483        if !matches!(self.peek(), Token::From) {
25484            return Err(self.err(format!(
25485                "expected FROM after EXTRACT field, got {:?}",
25486                self.peek()
25487            )));
25488        }
25489        self.advance();
25490        let source = self.parse_expr(0)?;
25491        if !matches!(self.peek(), Token::RParen) {
25492            return Err(self.err(format!(
25493                "expected ')' to close EXTRACT, got {:?}",
25494                self.peek()
25495            )));
25496        }
25497        self.advance();
25498        Ok(Expr::Extract {
25499            field,
25500            source: Box::new(source),
25501        })
25502    }
25503
25504    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
25505    /// is already consumed; we expect a single string literal next and
25506    /// resolve it into `Literal::Interval` at parse time so the engine
25507    /// never has to re-tokenise inside the string.
25508    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
25509    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
25510    /// is the SQL-standard form and is left to the path below.
25511    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
25512        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
25513        let (offset, sign) = match self.peek() {
25514            Token::Minus => (1, "-"),
25515            _ => (0, ""),
25516        };
25517        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
25518            return None;
25519        };
25520        self.tokens
25521            .get(self.pos + offset + 1)
25522            .filter(|t| mysql_interval_unit(t).is_some())?;
25523        Some((alloc::format!("{sign}{n}"), offset + 1))
25524    }
25525
25526    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
25527    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
25528    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
25529    ///
25530    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
25531    /// this by parsing the group and then restoring `self.pos` — which could
25532    /// never have worked, because `advance()` DESTROYS the token it returns
25533    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
25534    /// inert only because both branches errored back then.
25535    fn interval_paren_is_quantity(&self) -> bool {
25536        let mut depth = 0usize;
25537        let mut saw_top_level_comma = false;
25538        let mut i = self.pos;
25539        while let Some(tok) = self.tokens.get(i) {
25540            match tok {
25541                Token::LParen => depth += 1,
25542                Token::RParen => {
25543                    depth = depth.saturating_sub(1);
25544                    if depth == 0 {
25545                        return !saw_top_level_comma
25546                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
25547                                .is_some();
25548                    }
25549                }
25550                // A comma directly inside the outermost parens means the
25551                // argument list of the INTERVAL() function.
25552                Token::Comma if depth == 1 => saw_top_level_comma = true,
25553                Token::Eof => return false,
25554                _ => {}
25555            }
25556            i += 1;
25557        }
25558        false
25559    }
25560
25561    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
25562        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
25563        // (the index of the last Ni ≤ N), distinct from the interval literal.
25564        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
25565        // is decided by a non-destructive lookahead (round 422) before either
25566        // branch consumes anything. MySQL only.
25567        if self.mysql_dialect
25568            && matches!(self.peek(), Token::LParen)
25569            && !self.interval_paren_is_quantity()
25570        {
25571            self.advance(); // (
25572            let mut args = Vec::new();
25573            if !matches!(self.peek(), Token::RParen) {
25574                loop {
25575                    args.push(self.parse_expr(0)?);
25576                    if matches!(self.peek(), Token::Comma) {
25577                        self.advance();
25578                        continue;
25579                    }
25580                    break;
25581                }
25582            }
25583            if !matches!(self.peek(), Token::RParen) {
25584                return Err(self.err(alloc::format!(
25585                    "expected ')' after INTERVAL() arguments, got {:?}",
25586                    self.peek()
25587                )));
25588            }
25589            self.advance(); // )
25590            return Ok(Expr::FunctionCall {
25591                name: alloc::string::String::from("interval"),
25592                args,
25593            });
25594        }
25595        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
25596        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
25597        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
25598        // writes every date arithmetic there is, and it did not parse at
25599        // all. PG rejects the unquoted form outright (`syntax error at or
25600        // near "1"`, measured), so it is taken only in the MySQL dialect —
25601        // PG's own `INTERVAL '1' DAY` is untouched below.
25602        if self.mysql_dialect
25603            && let Some((text, consume)) = self.peek_unquoted_interval_count()
25604        {
25605            for _ in 0..consume {
25606                self.advance(); // the optional `-` and the number
25607            }
25608            let Some(unit) = mysql_interval_unit(self.peek()) else {
25609                return Err(self.err(alloc::format!(
25610                    "expected an interval unit after INTERVAL {text}, got {:?}",
25611                    self.peek()
25612                )));
25613            };
25614            self.advance(); // the unit
25615            let (months, days, micros) = scale_mysql_interval(&text, unit)
25616                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
25617            return Ok(Expr::Literal(Literal::Interval {
25618                months,
25619                days,
25620                micros,
25621                // The canonical rendering, so Display round-trips into a
25622                // form both dialects read back.
25623                text: alloc::format!("{text} {unit}"),
25624            }));
25625        }
25626        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
25627        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
25628        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
25629        // Those cannot fold into a compile-time `Literal::Interval`, so they
25630        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
25631        // builtin, which builds the value at run time (and yields NULL for a
25632        // NULL quantity, as MariaDB does). The literal path above still folds
25633        // the constant case — it is cheaper and round-trips through Display.
25634        //
25635        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
25636        // MySQL's quoted spelling) keep the qualifier path below.
25637        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
25638            let qty = self.parse_expr(0)?;
25639            let Some(unit) = mysql_interval_unit(self.peek()) else {
25640                return Err(self.err(alloc::format!(
25641                    "expected an interval unit after INTERVAL <expr>, got {:?}",
25642                    self.peek()
25643                )));
25644            };
25645            self.advance(); // the unit
25646            return Ok(make_interval_call(qty, unit));
25647        }
25648        let tok = self.advance();
25649        let Token::String(text) = tok else {
25650            return Err(self.err(format!(
25651                "expected string literal after INTERVAL, got {tok:?}"
25652            )));
25653        };
25654        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
25655        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
25656        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
25657        // bare number means and the leading/trailing precision.
25658        let field1 = interval_field_of(self.peek());
25659        let qualifier = if let Some(f1) = field1 {
25660            self.advance();
25661            let f2 = if matches!(self.peek(), Token::To) {
25662                self.advance();
25663                let Some(f) = interval_field_of(self.peek()) else {
25664                    return Err(self.err(format!(
25665                        "expected an interval field after TO, got {:?}",
25666                        self.peek()
25667                    )));
25668                };
25669                self.advance();
25670                Some(f)
25671            } else {
25672                None
25673            };
25674            Some((f1, f2))
25675        } else {
25676            None
25677        };
25678        let (months, days, micros) = match qualifier {
25679            Some(q) => interpret_qualified_interval(&text, q),
25680            None => parse_interval_text(&text),
25681        }
25682        .ok_or_else(|| ParseError {
25683            message: format!(
25684                "cannot parse INTERVAL {text:?}; \
25685                     expected `<n> <unit> [<n> <unit> ...]` with units \
25686                     microsecond[s], millisecond[s], second[s], minute[s], \
25687                     hour[s], day[s], week[s], month[s], year[s]"
25688            ),
25689            token_pos: self.consumed_pos(),
25690        })?;
25691        Ok(Expr::Literal(Literal::Interval {
25692            months,
25693            days,
25694            micros,
25695            text,
25696        }))
25697    }
25698
25699    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
25700    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
25701    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
25702    /// than a pgvector literal.
25703    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
25704        self.advance(); // consume `[`
25705        let mut items: Vec<Expr> = Vec::new();
25706        if !matches!(self.peek(), Token::RBracket) {
25707            loop {
25708                if matches!(self.peek(), Token::LBracket) {
25709                    items.push(self.parse_array_bracket_body()?);
25710                } else {
25711                    items.push(self.parse_expr(0)?);
25712                }
25713                match self.peek() {
25714                    Token::Comma => {
25715                        self.advance();
25716                    }
25717                    Token::RBracket => break,
25718                    other => {
25719                        return Err(self.err(alloc::format!(
25720                            "expected ',' or ']' in array literal, got {other:?}"
25721                        )));
25722                    }
25723                }
25724            }
25725        }
25726        self.advance(); // consume `]`
25727        Ok(Expr::Array(items))
25728    }
25729
25730    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
25731        let mut elems = Vec::new();
25732        if matches!(self.peek(), Token::RBracket) {
25733            self.advance();
25734            return Ok(Expr::Literal(Literal::Vector(elems)));
25735        }
25736        loop {
25737            let e = self.parse_expr(0)?;
25738            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
25739                message: format!("vector element must be a numeric literal, got {e:?}"),
25740                token_pos: self.pos,
25741            })?;
25742            elems.push(x);
25743            match self.peek() {
25744                Token::Comma => {
25745                    self.advance();
25746                }
25747                Token::RBracket => {
25748                    self.advance();
25749                    break;
25750                }
25751                other => {
25752                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
25753                }
25754            }
25755        }
25756        Ok(Expr::Literal(Literal::Vector(elems)))
25757    }
25758
25759    /// Atom that started with an identifier: could be `t.col`, `col`, or
25760    /// `func(arg, ...)`. Detect each shape by looking at the next token.
25761    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
25762    /// [, ...])`. Caller has already consumed `OVER`. Either clause
25763    /// is optional; an empty `()` is also legal (PG semantics).
25764    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
25765    /// modifier between `name(args)` and `OVER (...)`. Default is
25766    /// `Respect`. Unrecognised idents leave the stream unchanged.
25767    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
25768        let Token::Ident(s) = self.peek().clone() else {
25769            return NullTreatment::Respect;
25770        };
25771        let is_ignore = s.eq_ignore_ascii_case("ignore");
25772        let is_respect = s.eq_ignore_ascii_case("respect");
25773        if !is_ignore && !is_respect {
25774            return NullTreatment::Respect;
25775        }
25776        // Lookahead for NULLS — only consume both tokens together.
25777        // pos+1 must hold a "nulls" ident.
25778        if self.pos + 1 < self.tokens.len()
25779            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
25780            && s2.eq_ignore_ascii_case("nulls")
25781        {
25782            self.advance();
25783            self.advance();
25784            return if is_ignore {
25785                NullTreatment::Ignore
25786            } else {
25787                NullTreatment::Respect
25788            };
25789        }
25790        NullTreatment::Respect
25791    }
25792
25793    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
25794    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
25795    /// (same shape as the `OVER` tail). Consumes the whole clause and
25796    /// returns the predicate; returns `None` when no `FILTER` follows.
25797    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
25798        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25799            return Ok(None);
25800        };
25801        if !s.eq_ignore_ascii_case("filter") {
25802            return Ok(None);
25803        }
25804        self.advance(); // FILTER
25805        if !matches!(self.peek(), Token::LParen) {
25806            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
25807        }
25808        self.advance(); // (
25809        if !matches!(self.peek(), Token::Where) {
25810            return Err(self.err(format!(
25811                "expected WHERE inside FILTER (...), got {:?}",
25812                self.peek()
25813            )));
25814        }
25815        self.advance(); // WHERE
25816        let cond = self.parse_expr(0)?;
25817        if !matches!(self.peek(), Token::RParen) {
25818            return Err(self.err(format!(
25819                "expected ')' to close FILTER (WHERE ...), got {:?}",
25820                self.peek()
25821            )));
25822        }
25823        self.advance(); // )
25824        Ok(Some(Box::new(cond)))
25825    }
25826
25827    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
25828    /// the separator as the aggregate's second argument, which is the
25829    /// shape `string_agg` already takes. Returns whether one was there.
25830    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
25831        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
25832            return Ok(false);
25833        }
25834        self.advance();
25835        let Token::String(sep) = self.peek().clone() else {
25836            return Err(self.err(alloc::format!(
25837                "expected a string literal after SEPARATOR, got {:?}",
25838                self.peek()
25839            )));
25840        };
25841        self.advance();
25842        args.push(Expr::Literal(Literal::String(sep)));
25843        Ok(true)
25844    }
25845
25846    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
25847    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
25848    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
25849    /// keys, or an empty vec when no `WITHIN GROUP` follows.
25850    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
25851        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25852            return Ok(Vec::new());
25853        };
25854        if !s.eq_ignore_ascii_case("within") {
25855            return Ok(Vec::new());
25856        }
25857        self.advance(); // WITHIN
25858        if !matches!(self.peek(), Token::Group) {
25859            return Err(self.err(format!(
25860                "expected GROUP after WITHIN, got {:?}",
25861                self.peek()
25862            )));
25863        }
25864        self.advance(); // GROUP
25865        if !matches!(self.peek(), Token::LParen) {
25866            return Err(self.err(format!(
25867                "expected '(' after WITHIN GROUP, got {:?}",
25868                self.peek()
25869            )));
25870        }
25871        self.advance(); // (
25872        if !matches!(self.peek(), Token::Order) {
25873            return Err(self.err(format!(
25874                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
25875                self.peek()
25876            )));
25877        }
25878        self.advance(); // ORDER
25879        if !self.peek_is_by() {
25880            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25881        }
25882        self.advance(); // BY
25883        let mut keys: Vec<OrderBy> = Vec::new();
25884        loop {
25885            // v7.39 (round 691) — save/restore, the discipline this parser
25886            // already uses around `pending_sample_preds`, so a subquery inside
25887            // a key neither inherits nor leaks the channel.
25888            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25889            let saved_coll = self.order_key_collation.take();
25890            let parsed = self.parse_expr(0);
25891            self.in_order_by_key = saved_flag;
25892            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
25893            let expr = parsed?;
25894            let desc = if matches!(self.peek(), Token::Desc) {
25895                self.advance();
25896                true
25897            } else if matches!(self.peek(), Token::Asc) {
25898                self.advance();
25899                false
25900            } else {
25901                false
25902            };
25903            let nulls_first = self.parse_optional_nulls_placement()?;
25904            keys.push(OrderBy {
25905                expr,
25906                desc,
25907                nulls_first,
25908                collation,
25909            });
25910            if matches!(self.peek(), Token::Comma) {
25911                self.advance();
25912            } else {
25913                break;
25914            }
25915        }
25916        if !matches!(self.peek(), Token::RParen) {
25917            return Err(self.err(format!(
25918                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
25919                self.peek()
25920            )));
25921        }
25922        self.advance(); // )
25923        Ok(keys)
25924    }
25925
25926    /// No frame clause is supported.
25927    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
25928    fn parse_over_clause(
25929        &mut self,
25930    ) -> Result<
25931        (
25932            Vec<Expr>,
25933            Vec<(Expr, bool, Option<bool>)>,
25934            Option<WindowFrame>,
25935        ),
25936        ParseError,
25937    > {
25938        // `OVER w` — a named-window reference. The WINDOW clause
25939        // parses after the select list, so the name rides out as a
25940        // marker in partition_by; parse_bare_select substitutes the
25941        // definition once the clause is known.
25942        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
25943            let name = w.clone();
25944            self.advance();
25945            return Ok((
25946                alloc::vec![Expr::Column(crate::ast::ColumnName {
25947                    qualifier: Some("__named_window__".to_string()),
25948                    name,
25949                })],
25950                Vec::new(),
25951                None,
25952            ));
25953        }
25954        if !matches!(self.peek(), Token::LParen) {
25955            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
25956        }
25957        self.advance();
25958        let mut partition_by = Vec::new();
25959        let mut order_by = Vec::new();
25960        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
25961        // window, refined in place. PG's rules (probed against 18.4) differ
25962        // from the bare `OVER w1` form, so the reference rides out under its
25963        // own marker and `substitute_named_windows` applies them. The base
25964        // name is any leading identifier that isn't a window-spec keyword.
25965        let base_window = match self.peek() {
25966            Token::Ident(s) | Token::QuotedIdent(s)
25967                if !s.eq_ignore_ascii_case("partition")
25968                    && !s.eq_ignore_ascii_case("rows")
25969                    && !s.eq_ignore_ascii_case("range")
25970                    && !s.eq_ignore_ascii_case("groups") =>
25971            {
25972                let n = s.clone();
25973                self.advance();
25974                Some(n)
25975            }
25976            _ => None,
25977        };
25978        // PARTITION BY ?
25979        // v7.37.6-B promoted PARTITION to a reserved keyword
25980        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25981        // `Token::Ident("partition")`. Accept both so older sources
25982        // and the new lexer surface land on the same path.
25983        let is_partition_kw = match self.peek() {
25984            Token::Partition => true,
25985            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25986            _ => false,
25987        };
25988        if is_partition_kw {
25989            self.advance();
25990            if !self.peek_is_by() {
25991                return Err(self.err(format!(
25992                    "expected BY after PARTITION, got {:?}",
25993                    self.peek()
25994                )));
25995            }
25996            self.advance();
25997            loop {
25998                partition_by.push(self.parse_expr(0)?);
25999                if matches!(self.peek(), Token::Comma) {
26000                    self.advance();
26001                    continue;
26002                }
26003                break;
26004            }
26005        }
26006        // ORDER BY ?
26007        if matches!(self.peek(), Token::Order) {
26008            self.advance();
26009            if !self.peek_is_by() {
26010                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
26011            }
26012            self.advance();
26013            loop {
26014                let e = self.parse_expr(0)?;
26015                let desc = if matches!(self.peek(), Token::Desc) {
26016                    self.advance();
26017                    true
26018                } else if matches!(self.peek(), Token::Asc) {
26019                    self.advance();
26020                    false
26021                } else {
26022                    false
26023                };
26024                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
26025                let nulls_first = self.parse_optional_nulls_placement()?;
26026                order_by.push((e, desc, nulls_first));
26027                if matches!(self.peek(), Token::Comma) {
26028                    self.advance();
26029                    continue;
26030                }
26031                break;
26032            }
26033        }
26034        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
26035        // Both keywords come through the lexer as identifiers; match
26036        // case-insensitively.
26037        let mut frame: Option<WindowFrame> = None;
26038        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
26039            let kind = if s.eq_ignore_ascii_case("rows") {
26040                Some(FrameKind::Rows)
26041            } else if s.eq_ignore_ascii_case("range") {
26042                Some(FrameKind::Range)
26043            } else if s.eq_ignore_ascii_case("groups") {
26044                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
26045                Some(FrameKind::Groups)
26046            } else {
26047                None
26048            };
26049            if let Some(kind) = kind {
26050                self.advance();
26051                frame = Some(self.parse_frame_tail(kind)?);
26052            }
26053        }
26054        if !matches!(self.peek(), Token::RParen) {
26055            return Err(self.err(format!(
26056                "expected ')' to close OVER clause, got {:?}",
26057                self.peek()
26058            )));
26059        }
26060        self.advance();
26061        if let Some(base) = base_window {
26062            // A copy may refine but never override the base's partitioning
26063            // (PG rejects it outright, before looking the name up).
26064            if !partition_by.is_empty() {
26065                return Err(self.err(alloc::format!(
26066                    "cannot override PARTITION BY clause of window \"{base}\""
26067                )));
26068            }
26069            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
26070                qualifier: Some("__named_window_ref__".to_string()),
26071                name: base,
26072            })];
26073        }
26074        Ok((partition_by, order_by, frame))
26075    }
26076
26077    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
26078    /// or `RANGE` keyword was just consumed. Accepts both
26079    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
26080    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
26081    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
26082    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
26083        let (start, end) = if matches!(self.peek(), Token::Between) {
26084            self.advance();
26085            let start = self.parse_frame_bound()?;
26086            if !matches!(self.peek(), Token::And) {
26087                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
26088            }
26089            self.advance();
26090            let end = self.parse_frame_bound()?;
26091            (start, Some(end))
26092        } else {
26093            (self.parse_frame_bound()?, None)
26094        };
26095        let exclude = self.parse_frame_exclusion()?;
26096        Ok(WindowFrame {
26097            kind,
26098            start,
26099            end,
26100            exclude,
26101        })
26102    }
26103
26104    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
26105    /// after a frame spec. NO OTHERS is the default no-op.
26106    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
26107        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
26108            return Ok(FrameExclusion::NoOthers);
26109        }
26110        self.advance(); // EXCLUDE
26111        match self.peek() {
26112            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
26113                self.advance();
26114                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
26115                    return Err(self.err(format!(
26116                        "expected ROW after EXCLUDE CURRENT, got {:?}",
26117                        self.peek()
26118                    )));
26119                }
26120                self.advance();
26121                Ok(FrameExclusion::CurrentRow)
26122            }
26123            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
26124            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
26125            // Without this arm it fell to the catch-all, whose message
26126            // self-contradictingly listed GROUP as expected.
26127            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
26128                self.advance();
26129                Ok(FrameExclusion::Group)
26130            }
26131            Token::Group => {
26132                self.advance();
26133                Ok(FrameExclusion::Group)
26134            }
26135            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
26136                self.advance();
26137                Ok(FrameExclusion::Ties)
26138            }
26139            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
26140                self.advance();
26141                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
26142                    return Err(self.err(format!(
26143                        "expected OTHERS after EXCLUDE NO, got {:?}",
26144                        self.peek()
26145                    )));
26146                }
26147                self.advance();
26148                Ok(FrameExclusion::NoOthers)
26149            }
26150            other => Err(self.err(format!(
26151                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
26152            ))),
26153        }
26154    }
26155
26156    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
26157    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
26158    /// `UNBOUNDED FOLLOWING`.
26159    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
26160        // Interval-typed offset for a value-based RANGE frame over a
26161        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
26162        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
26163        // PRECEDING`.
26164        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
26165            let dir = self.expect_ident_like()?;
26166            return if dir.eq_ignore_ascii_case("preceding") {
26167                Ok(FrameBound::IntervalPreceding {
26168                    months,
26169                    days,
26170                    micros,
26171                })
26172            } else if dir.eq_ignore_ascii_case("following") {
26173                Ok(FrameBound::IntervalFollowing {
26174                    months,
26175                    days,
26176                    micros,
26177                })
26178            } else {
26179                Err(self.err(format!(
26180                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
26181                )))
26182            };
26183        }
26184        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
26185        if let Token::Integer(n) = *self.peek() {
26186            self.advance();
26187            let n: u64 = u64::try_from(n).map_err(|_| {
26188                self.err(format!(
26189                    "invalid frame offset {n} — expected non-negative integer"
26190                ))
26191            })?;
26192            let dir = self.expect_ident_like()?;
26193            return if dir.eq_ignore_ascii_case("preceding") {
26194                Ok(FrameBound::OffsetPreceding(n))
26195            } else if dir.eq_ignore_ascii_case("following") {
26196                Ok(FrameBound::OffsetFollowing(n))
26197            } else {
26198                Err(self.err(format!(
26199                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
26200                )))
26201            };
26202        }
26203        let first = self.expect_ident_like()?;
26204        if first.eq_ignore_ascii_case("unbounded") {
26205            let dir = self.expect_ident_like()?;
26206            return if dir.eq_ignore_ascii_case("preceding") {
26207                Ok(FrameBound::UnboundedPreceding)
26208            } else if dir.eq_ignore_ascii_case("following") {
26209                Ok(FrameBound::UnboundedFollowing)
26210            } else {
26211                Err(self.err(format!(
26212                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
26213                )))
26214            };
26215        }
26216        if first.eq_ignore_ascii_case("current") {
26217            let row = self.expect_ident_like()?;
26218            if !row.eq_ignore_ascii_case("row") {
26219                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
26220            }
26221            return Ok(FrameBound::CurrentRow);
26222        }
26223        Err(self.err(format!(
26224            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
26225        )))
26226    }
26227
26228    /// Detect and consume a leading interval offset in a frame bound —
26229    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
26230    /// `(months, days, micros)`. Leaves the cursor on the trailing
26231    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
26232    /// when the next tokens are not an interval offset.
26233    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
26234        // Shape A — `INTERVAL '1 day'`.
26235        if matches!(self.peek(), Token::Interval) {
26236            self.advance(); // INTERVAL
26237            let atom = self.parse_interval_atom()?;
26238            if let Expr::Literal(Literal::Interval {
26239                months,
26240                days,
26241                micros,
26242                ..
26243            }) = atom
26244            {
26245                return Ok(Some((months, days, micros)));
26246            }
26247            return Err(self.err("expected an interval literal in frame offset".to_string()));
26248        }
26249        // Shape B — `'1 day'::interval`. Look ahead for the exact
26250        // string / `::` / interval-target triple before committing.
26251        if let Token::String(text) = self.peek() {
26252            let target_is_interval = match self.tokens.get(self.pos + 2) {
26253                Some(Token::Interval) => true,
26254                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
26255                _ => false,
26256            };
26257            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
26258                && target_is_interval;
26259            if is_cast {
26260                let text = text.clone();
26261                self.advance(); // string
26262                self.advance(); // ::
26263                self.advance(); // interval
26264                let parts = parse_interval_text(&text).ok_or_else(|| {
26265                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
26266                })?;
26267                return Ok(Some(parts));
26268            }
26269        }
26270        Ok(None)
26271    }
26272
26273    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
26274        // v7.39.2 — MySQL's charset INTRODUCER: `_utf8mb4'x'`, `N'y'`,
26275        // `_binary'z'`. All three were `ERROR 1064 syntax error` here
26276        // and all three answer the literal on MySQL 9.7.2.
26277        //
26278        // It is not only syntax, which is why it waited for
26279        // `Expr::Collate`: measured, `_binary'A' = 'a'` is 0 on MySQL
26280        // because `_binary` makes the comparison byte-wise, while
26281        // `_utf8mb4'A' = _utf8mb4'a'` is 1. Accepting the syntax and
26282        // dropping the charset would have turned a hard error into a
26283        // silently wrong comparison — worse than the error it replaced.
26284        //
26285        // An UNKNOWN charset is NOT an introducer: MySQL answers
26286        // `Unknown column '_nosuch'`, because it parses as a column
26287        // reference followed by a string. So the table decides, and it
26288        // is the same table `SET NAMES` reads.
26289        //
26290        // A space is allowed between the two (`_utf8mb4 'x'`), which
26291        // falls out of asking the token stream rather than the bytes.
26292        if self.mysql_dialect
26293            && let Token::String(_) = self.peek()
26294        {
26295            let lower = first.to_ascii_lowercase();
26296            let charset = if lower == "n" {
26297                // `N'…'` is the national character set, which MySQL
26298                // documents as utf8 — utf8mb3 in 9.7.2's spelling.
26299                //
26300                // utf8mb3 and utf8mb4 both fold case in their default
26301                // collations, so nothing SPG can be asked distinguishes
26302                // the two here: an ablation swapping this to utf8mb4
26303                // reddens no pin. Recorded rather than implied — the
26304                // spelling follows MySQL's documentation, not a
26305                // measurement.
26306                Some("utf8mb3")
26307            } else {
26308                // No filter here: the lookup below IS the test for
26309                // "is this a charset". An ablation that removed a filter
26310                // in this spot reddened nothing, which is how the two
26311                // were found to be one check written twice.
26312                lower.strip_prefix('_')
26313            };
26314            if let Some(cs) = charset
26315                && let Some(collation) = crate::charset::charset_default_collation(cs)
26316            {
26317                let Token::String(body) = self.advance() else {
26318                    unreachable!("peeked a string");
26319                };
26320                return Ok(Expr::Collate {
26321                    expr: Box::new(Expr::Literal(Literal::String(body))),
26322                    collation: String::from(collation),
26323                });
26324            }
26325        }
26326        if matches!(self.peek(), Token::Dot) {
26327            self.advance();
26328            let name = self.expect_ident_like()?;
26329            // v7.14.0 — schema-qualified function call
26330            // `<schema>.<fn>(args)`. PG dumps emit
26331            // `pg_catalog.set_config(...)` in the preamble. SPG
26332            // is single-namespace: drop the schema prefix and
26333            // route the dispatch on the bare function name.
26334            if matches!(self.peek(), Token::LParen) {
26335                return self.finish_ident_atom(name);
26336            }
26337            return Ok(Expr::Column(ColumnName {
26338                qualifier: Some(first),
26339                name,
26340            }));
26341        }
26342        if matches!(self.peek(), Token::LParen) {
26343            self.advance();
26344            // `COUNT(*)` — special-cased here because `*` isn't a normal
26345            // expression token. Lower-case match on `first` since the lexer
26346            // folds identifiers.
26347            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
26348                self.advance();
26349                if !matches!(self.peek(), Token::RParen) {
26350                    return Err(self.err(format!(
26351                        "expected ')' after COUNT(*), got {:?}",
26352                        self.peek()
26353                    )));
26354                }
26355                self.advance();
26356                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
26357                let filter = self.parse_filter_clause()?;
26358                // v4.12: COUNT(*) OVER (...) — same window tail.
26359                let null_treatment = self.parse_null_treatment_modifier();
26360                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26361                    && s.eq_ignore_ascii_case("over")
26362                {
26363                    self.advance();
26364                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
26365                    return Ok(Expr::WindowFunction {
26366                        name: "count_star".into(),
26367                        args: Vec::new(),
26368                        partition_by,
26369                        order_by,
26370                        frame,
26371                        null_treatment,
26372                        filter,
26373                    });
26374                }
26375                if let Some(filter) = filter {
26376                    return Ok(Expr::AggregateOrdered {
26377                        call: Box::new(Expr::FunctionCall {
26378                            name: "count_star".into(),
26379                            args: Vec::new(),
26380                        }),
26381                        order_by: Vec::new(),
26382                        distinct: false,
26383                        filter: Some(filter),
26384                    });
26385                }
26386                return Ok(Expr::FunctionCall {
26387                    name: "count_star".into(),
26388                    args: Vec::new(),
26389                });
26390            }
26391            // Function call. PG-style: zero-or-more comma-separated args.
26392            let mut args = Vec::new();
26393            // v7.38 (read01, T14) — named-argument notation `argname => value`.
26394            // Names are collected in lock-step with `args` and resolved to
26395            // positional order after the loop (the AST stays positional).
26396            let mut arg_names: Vec<Option<String>> = Vec::new();
26397            let mut agg_order_by: Vec<OrderBy> = Vec::new();
26398            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
26399            // seen, so the value arguments before it can be folded.
26400            let mut saw_separator = false;
26401            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
26402            // v7.32 (round-29) — accept the dual `ALL` quantifier too
26403            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
26404            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
26405                self.advance();
26406                true
26407            } else if matches!(self.peek(), Token::All) {
26408                self.advance();
26409                false
26410            } else {
26411                false
26412            };
26413            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
26414            // TIMESTAMPDIFF take a bare unit keyword as the first
26415            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
26416            // bare type keyword (DATE / TIME / DATETIME); lower them
26417            // onto string literals so the evaluator sees plain text.
26418            if ((first.eq_ignore_ascii_case("timestampadd")
26419                || first.eq_ignore_ascii_case("timestampdiff"))
26420                && matches!(self.peek(), Token::Ident(u) if matches!(
26421                    u.to_ascii_lowercase().as_str(),
26422                    "microsecond" | "second" | "minute" | "hour" | "day"
26423                        | "week" | "month" | "quarter" | "year"
26424                )))
26425                || (first.eq_ignore_ascii_case("get_format")
26426                    && matches!(self.peek(), Token::Ident(u) if matches!(
26427                        u.to_ascii_lowercase().as_str(),
26428                        "date" | "time" | "datetime" | "timestamp"
26429                    )))
26430            {
26431                if let Token::Ident(u) = self.peek() {
26432                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
26433                }
26434                self.advance();
26435                if matches!(self.peek(), Token::Comma) {
26436                    self.advance();
26437                }
26438            }
26439            // `ROW(a, b, …)` keyword constructor. Followed by a
26440            // comparison operator or [NOT] IN it joins the paren
26441            // row-constructor machinery (fieldwise parse-time
26442            // expansion); bare, it stays a `row` call the evaluator
26443            // renders as PG record text.
26444            if first.eq_ignore_ascii_case("row") {
26445                let mut row_items = Vec::new();
26446                if !matches!(self.peek(), Token::RParen) {
26447                    loop {
26448                        row_items.push(self.parse_expr(0)?);
26449                        match self.peek() {
26450                            Token::Comma => {
26451                                self.advance();
26452                            }
26453                            Token::RParen => break,
26454                            other => {
26455                                return Err(self.err(format!(
26456                                    "expected ',' or ')' in ROW(...), got {other:?}"
26457                                )));
26458                            }
26459                        }
26460                    }
26461                }
26462                self.advance(); // ')'
26463                let comparison_follows = matches!(
26464                    self.peek(),
26465                    Token::Eq
26466                        | Token::NotEq
26467                        | Token::Lt
26468                        | Token::LtEq
26469                        | Token::Gt
26470                        | Token::GtEq
26471                        | Token::In
26472                ) || (matches!(self.peek(), Token::Not)
26473                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
26474                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
26475                if comparison_follows && !row_items.is_empty() {
26476                    return self.parse_row_comparison_tail(row_items);
26477                }
26478                return Ok(Expr::FunctionCall {
26479                    name: String::from("row"),
26480                    args: row_items,
26481                });
26482            }
26483            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
26484            // the parse-mode keyword introduces the source text. SPG
26485            // carries XML as text, so both modes lower to __xmlparse(expr)
26486            // which validates well-formedness and returns Value::Xml.
26487            if first.eq_ignore_ascii_case("xmlparse")
26488                && matches!(self.peek(), Token::Ident(kw)
26489                    if kw.eq_ignore_ascii_case("document")
26490                        || kw.eq_ignore_ascii_case("content"))
26491            {
26492                let mode = match self.advance() {
26493                    Token::Ident(kw) => kw.to_ascii_lowercase(),
26494                    _ => unreachable!("peeked an ident"),
26495                };
26496                let src = self.parse_expr(0)?;
26497                if !matches!(self.peek(), Token::RParen) {
26498                    return Err(self.err(format!(
26499                        "expected ')' to close XMLPARSE, got {:?}",
26500                        self.peek()
26501                    )));
26502                }
26503                self.advance();
26504                return Ok(Expr::FunctionCall {
26505                    name: String::from("__xmlparse"),
26506                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
26507                });
26508            }
26509            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
26510            // keyword introduces the element name (a bare or quoted
26511            // identifier), then optional content expressions. Lower to a
26512            // plain `xmlelement(name_text, content …)` call.
26513            if first.eq_ignore_ascii_case("xmlelement")
26514                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
26515            {
26516                self.advance(); // consume NAME
26517                let elem_name = match self.peek().clone() {
26518                    Token::Ident(n) | Token::QuotedIdent(n) => {
26519                        self.advance();
26520                        n
26521                    }
26522                    other => {
26523                        return Err(self.err(format!(
26524                            "expected element name after XMLELEMENT NAME, got {other:?}"
26525                        )));
26526                    }
26527                };
26528                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
26529                while matches!(self.peek(), Token::Comma) {
26530                    self.advance();
26531                    args.push(self.parse_expr(0)?);
26532                }
26533                if !matches!(self.peek(), Token::RParen) {
26534                    return Err(self.err(format!(
26535                        "expected ')' to close XMLELEMENT, got {:?}",
26536                        self.peek()
26537                    )));
26538                }
26539                self.advance();
26540                return Ok(Expr::FunctionCall {
26541                    name: String::from("xmlelement"),
26542                    args,
26543                });
26544            }
26545            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
26546            // becomes a `<name>value</name>` element; a bare column infers its
26547            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
26548            // v7.39.2 — MySQL's two CONVERT forms, neither of which parsed.
26549            // `CONVERT(expr USING cs)` was a syntax error at USING, and
26550            // `CONVERT(expr, CHAR)` was read as PostgreSQL's three-argument
26551            // `convert(bytea, src, dest)` and answered `column "char" does
26552            // not exist`. Both are casts in MySQL: measured on 9.7.2,
26553            // `CONVERT(0x41 USING utf8mb4)` and `CONVERT(0x41, CHAR)` are
26554            // both 'A', and `CONVERT(123, CHAR)` is '123'.
26555            //
26556            // The charset is checked against the same table the introducers
26557            // use, so an unknown one is refused rather than quietly ignored.
26558            if self.mysql_dialect
26559                && first.eq_ignore_ascii_case("convert")
26560                && !matches!(self.peek(), Token::RParen)
26561            {
26562                let save = self.pos;
26563                let inner = self.parse_expr(0)?;
26564                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
26565                    self.advance();
26566                    let cs = match self.peek().clone() {
26567                        Token::Ident(n) | Token::QuotedIdent(n) => {
26568                            self.advance();
26569                            n
26570                        }
26571                        other => {
26572                            return Err(self.err(alloc::format!(
26573                                "expected a charset after USING, got {other:?}"
26574                            )));
26575                        }
26576                    };
26577                    let lc = cs.to_ascii_lowercase();
26578                    if lc != "binary" && crate::charset::charset_default_collation(&lc).is_none() {
26579                        return Err(self.err(alloc::format!("unknown character set: '{cs}'")));
26580                    }
26581                    if !matches!(self.peek(), Token::RParen) {
26582                        return Err(self.err(alloc::format!(
26583                            "expected ')' after CONVERT … USING, got {:?}",
26584                            self.peek()
26585                        )));
26586                    }
26587                    self.advance();
26588                    let target = if lc == "binary" {
26589                        CastTarget::Named("binary".to_string())
26590                    } else {
26591                        CastTarget::Text
26592                    };
26593                    return self.finish_postfix_casts(Expr::Cast {
26594                        expr: alloc::boxed::Box::new(inner),
26595                        target,
26596                    });
26597                }
26598                if matches!(self.peek(), Token::Comma) {
26599                    self.advance();
26600                    // A type name here is MySQL's cast form; anything else
26601                    // (three string arguments) is PostgreSQL's `convert`,
26602                    // which keeps its own path.
26603                    if let Ok(target) = self.parse_cast_target()
26604                        && matches!(self.peek(), Token::RParen)
26605                    {
26606                        self.advance();
26607                        return self.finish_postfix_casts(Expr::Cast {
26608                            expr: alloc::boxed::Box::new(inner),
26609                            target,
26610                        });
26611                    }
26612                }
26613                self.pos = save;
26614            }
26615            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
26616                let mut args: Vec<Expr> = Vec::new();
26617                loop {
26618                    let val = self.parse_expr(0)?;
26619                    let name = if matches!(self.peek(), Token::As) {
26620                        self.advance();
26621                        match self.peek().clone() {
26622                            Token::Ident(n) | Token::QuotedIdent(n) => {
26623                                self.advance();
26624                                n
26625                            }
26626                            other => {
26627                                return Err(self.err(format!(
26628                                    "expected name after AS in XMLFOREST, got {other:?}"
26629                                )));
26630                            }
26631                        }
26632                    } else if let Expr::Column(c) = &val {
26633                        c.name.clone()
26634                    } else {
26635                        return Err(
26636                            self.err("XMLFOREST element without a column name needs AS".into())
26637                        );
26638                    };
26639                    args.push(Expr::Literal(Literal::String(name)));
26640                    args.push(val);
26641                    if matches!(self.peek(), Token::Comma) {
26642                        self.advance();
26643                    } else {
26644                        break;
26645                    }
26646                }
26647                if !matches!(self.peek(), Token::RParen) {
26648                    return Err(self.err(format!(
26649                        "expected ')' to close XMLFOREST, got {:?}",
26650                        self.peek()
26651                    )));
26652                }
26653                self.advance();
26654                return Ok(Expr::FunctionCall {
26655                    name: String::from("xmlforest"),
26656                    args,
26657                });
26658            }
26659            // SQL-standard `POSITION(sub IN str)` — lowers onto
26660            // strpos(str, sub). IN is the argument separator here,
26661            // so the needle parses with the IN-tail suppressed.
26662            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
26663                let saved = self.suppress_in_tail;
26664                self.suppress_in_tail = true;
26665                let needle = self.parse_expr(0);
26666                self.suppress_in_tail = saved;
26667                let needle = needle?;
26668                if matches!(self.peek(), Token::In) {
26669                    self.advance();
26670                    let haystack = self.parse_expr(0)?;
26671                    if !matches!(self.peek(), Token::RParen) {
26672                        return Err(self.err(format!(
26673                            "expected ')' to close POSITION, got {:?}",
26674                            self.peek()
26675                        )));
26676                    }
26677                    self.advance();
26678                    return Ok(Expr::FunctionCall {
26679                        name: String::from("strpos"),
26680                        args: alloc::vec![haystack, needle],
26681                    });
26682                }
26683                // position(sub, str) comma form (incl. bytea) —
26684                // hand the parsed first arg to the generic list.
26685                args.push(needle);
26686                if matches!(self.peek(), Token::Comma) {
26687                    self.advance();
26688                }
26689            }
26690            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
26691            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
26692            // plain comma forms TRIM(str) / TRIM(str, chars) keep
26693            // riding the generic argument list below.
26694            if first.eq_ignore_ascii_case("trim") {
26695                let mode = match self.peek() {
26696                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
26697                        self.advance();
26698                        Some("btrim")
26699                    }
26700                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
26701                        self.advance();
26702                        Some("ltrim")
26703                    }
26704                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
26705                        self.advance();
26706                        Some("rtrim")
26707                    }
26708                    _ => None,
26709                };
26710                if mode.is_some() || matches!(self.peek(), Token::From) {
26711                    // TRIM([mode] FROM str) — no strip-chars.
26712                    let chars = if matches!(self.peek(), Token::From) {
26713                        None
26714                    } else {
26715                        Some(self.parse_expr(0)?)
26716                    };
26717                    if !matches!(self.peek(), Token::From) {
26718                        return Err(self.err(format!(
26719                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
26720                            self.peek()
26721                        )));
26722                    }
26723                    self.advance();
26724                    let target = self.parse_expr(0)?;
26725                    if !matches!(self.peek(), Token::RParen) {
26726                        return Err(
26727                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
26728                        );
26729                    }
26730                    self.advance();
26731                    let mut trim_args = alloc::vec![target];
26732                    if let Some(c) = chars {
26733                        trim_args.push(c);
26734                    }
26735                    return Ok(Expr::FunctionCall {
26736                        name: String::from(mode.unwrap_or("btrim")),
26737                        args: trim_args,
26738                    });
26739                }
26740            }
26741            if !matches!(self.peek(), Token::RParen) {
26742                loop {
26743                    // v7.38 (read01, T14) — `argname => value` names this arg.
26744                    // v7.39 (read01 round 77) — `argname := value` is the same
26745                    // thing, and it is the spelling PG's own docs lead with. It
26746                    // was simply never lexed here, so every `f(x := 1)` died in
26747                    // the parser regardless of what `f` was.
26748                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
26749                        (
26750                            Token::Ident(n) | Token::QuotedIdent(n),
26751                            Some(Token::FatArrow | Token::ColonEq),
26752                        ) => {
26753                            let name = n.clone();
26754                            self.advance(); // name
26755                            self.advance(); // => / :=
26756                            Some(name)
26757                        }
26758                        _ => None,
26759                    };
26760                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
26761                    // array's elements into a variadic call's trailing args
26762                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
26763                    // reserved, so it arrives as a bare ident before the arg.
26764                    let is_variadic = this_name.is_none()
26765                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
26766                    if is_variadic {
26767                        self.advance();
26768                    }
26769                    let arg = self.parse_expr(0)?;
26770                    args.push(match &this_name {
26771                        // The callee's parameter names decide the slot, and a
26772                        // user function's live in the catalog. Carry the name
26773                        // to eval rather than guessing here.
26774                        Some(n) => Expr::NamedArg {
26775                            name: n.clone(),
26776                            expr: Box::new(arg),
26777                        },
26778                        None if is_variadic => Expr::Variadic(Box::new(arg)),
26779                        None => arg,
26780                    });
26781                    arg_names.push(this_name);
26782                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
26783                    // The `::` cast already worked; this lowers the
26784                    // function form onto the same Expr::Cast node.
26785                    if first.eq_ignore_ascii_case("cast")
26786                        && args.len() == 1
26787                        && matches!(self.peek(), Token::As)
26788                    {
26789                        self.advance();
26790                        let target = self.parse_cast_target()?;
26791                        if !matches!(self.peek(), Token::RParen) {
26792                            return Err(self.err(format!(
26793                                "expected ')' to close CAST, got {:?}",
26794                                self.peek()
26795                            )));
26796                        }
26797                        self.advance();
26798                        return Ok(Expr::Cast {
26799                            expr: Box::new(args.pop().expect("one arg")),
26800                            target,
26801                        });
26802                    }
26803                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
26804                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
26805                    // keywords; SPG's lexer makes them plain idents (so they'd be
26806                    // read as column refs). Lower the keyword to the string form
26807                    // the evaluator already accepts.
26808                    if first.eq_ignore_ascii_case("normalize")
26809                        && args.len() == 1
26810                        && matches!(self.peek(), Token::Comma)
26811                    {
26812                        let form = match self.tokens.get(self.pos + 1) {
26813                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
26814                                let up = f.to_ascii_uppercase();
26815                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
26816                            }
26817                            _ => None,
26818                        };
26819                        if let Some(up) = form {
26820                            self.advance(); // comma
26821                            self.advance(); // form keyword
26822                            args.push(Expr::Literal(Literal::String(up)));
26823                        }
26824                    }
26825                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
26826                    // form. Desugars to the comma-list shape evaluator already
26827                    // handles. Triggered after the first arg when the function
26828                    // name is substring / substr and the next token is FROM
26829                    // (a reserved keyword in PG; SPG also reserves it).
26830                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
26831                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
26832                    // internal __substring_similar(str, pat, esc) call.
26833                    if (first.eq_ignore_ascii_case("substring")
26834                        || first.eq_ignore_ascii_case("substr"))
26835                        && args.len() == 1
26836                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
26837                    {
26838                        self.advance(); // SIMILAR
26839                        let pattern = self.parse_expr(0)?;
26840                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
26841                        {
26842                            return Err(self.err(format!(
26843                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
26844                                self.peek()
26845                            )));
26846                        }
26847                        self.advance(); // ESCAPE
26848                        let esc = self.parse_expr(0)?;
26849                        if !matches!(self.peek(), Token::RParen) {
26850                            return Err(self.err(format!(
26851                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
26852                                self.peek()
26853                            )));
26854                        }
26855                        self.advance();
26856                        args.push(pattern);
26857                        args.push(esc);
26858                        return Ok(Expr::FunctionCall {
26859                            name: "__substring_similar".to_string(),
26860                            args,
26861                        });
26862                    }
26863                    if (first.eq_ignore_ascii_case("substring")
26864                        || first.eq_ignore_ascii_case("substr"))
26865                        && args.len() == 1
26866                        && matches!(self.peek(), Token::From | Token::For)
26867                    {
26868                        // `substring(str FROM pos [FOR len])`, or the FOR-only
26869                        // `substring(str FOR len)` which PG treats as FROM 1.
26870                        if matches!(self.peek(), Token::From) {
26871                            self.advance();
26872                            let start = self.parse_expr(0)?;
26873                            args.push(start);
26874                        } else {
26875                            args.push(Expr::Literal(Literal::Integer(1)));
26876                        }
26877                        if matches!(self.peek(), Token::For) {
26878                            self.advance();
26879                            let length = self.parse_expr(0)?;
26880                            args.push(length);
26881                        }
26882                        if !matches!(self.peek(), Token::RParen) {
26883                            return Err(self.err(format!(
26884                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
26885                                self.peek()
26886                            )));
26887                        }
26888                        self.advance();
26889                        return Ok(Expr::FunctionCall {
26890                            name: first.to_ascii_lowercase(),
26891                            args,
26892                        });
26893                    }
26894                    // PG `overlay(str PLACING repl FROM n [FOR len])`
26895                    // syntactic form. Desugars to the `overlay(str,
26896                    // repl, n[, len])` comma-list shape the evaluator
26897                    // already implements. `PLACING` is not a reserved
26898                    // token in SPG, so it arrives as a bare Ident.
26899                    if first.eq_ignore_ascii_case("overlay")
26900                        && args.len() == 1
26901                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
26902                    {
26903                        self.advance(); // consume PLACING
26904                        args.push(self.parse_expr(0)?); // replacement
26905                        if !matches!(self.peek(), Token::From) {
26906                            return Err(self.err(format!(
26907                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
26908                                self.peek()
26909                            )));
26910                        }
26911                        self.advance();
26912                        args.push(self.parse_expr(0)?); // start position
26913                        if matches!(self.peek(), Token::For) {
26914                            self.advance();
26915                            args.push(self.parse_expr(0)?); // length
26916                        }
26917                        if !matches!(self.peek(), Token::RParen) {
26918                            return Err(self.err(format!(
26919                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
26920                                self.peek()
26921                            )));
26922                        }
26923                        self.advance();
26924                        return Ok(Expr::FunctionCall {
26925                            name: String::from("overlay"),
26926                            args,
26927                        });
26928                    }
26929                    // `TRIM(chars FROM str)` — the keyword-less
26930                    // spelling lands here after the chars parse
26931                    // (the keyword forms return earlier).
26932                    if first.eq_ignore_ascii_case("trim")
26933                        && args.len() == 1
26934                        && matches!(self.peek(), Token::From)
26935                    {
26936                        self.advance();
26937                        let target = self.parse_expr(0)?;
26938                        if !matches!(self.peek(), Token::RParen) {
26939                            return Err(self.err(format!(
26940                                "expected ')' to close TRIM(chars FROM str), got {:?}",
26941                                self.peek()
26942                            )));
26943                        }
26944                        self.advance();
26945                        let chars = args.pop().expect("one arg");
26946                        return Ok(Expr::FunctionCall {
26947                            name: String::from("btrim"),
26948                            args: alloc::vec![target, chars],
26949                        });
26950                    }
26951                    // v7.24 (round-16 A) — aggregate-internal
26952                    // ordering: `array_agg(x ORDER BY y DESC NULLS
26953                    // LAST)`. Keys close the argument list.
26954                    if matches!(self.peek(), Token::Order) {
26955                        self.advance();
26956                        if !self.peek_is_by() {
26957                            return Err(self.err(format!(
26958                                "expected BY after ORDER in aggregate args, got {:?}",
26959                                self.peek()
26960                            )));
26961                        }
26962                        self.advance();
26963                        loop {
26964                            // v7.39 (round 691) — save/restore, the discipline this parser
26965                            // already uses around `pending_sample_preds`, so a subquery inside
26966                            // a key neither inherits nor leaks the channel.
26967                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
26968                            let saved_coll = self.order_key_collation.take();
26969                            let parsed = self.parse_expr(0);
26970                            self.in_order_by_key = saved_flag;
26971                            let collation =
26972                                core::mem::replace(&mut self.order_key_collation, saved_coll);
26973                            let expr = parsed?;
26974                            let desc = if matches!(self.peek(), Token::Desc) {
26975                                self.advance();
26976                                true
26977                            } else if matches!(self.peek(), Token::Asc) {
26978                                self.advance();
26979                                false
26980                            } else {
26981                                false
26982                            };
26983                            let nulls_first = self.parse_optional_nulls_placement()?;
26984                            agg_order_by.push(OrderBy {
26985                                expr,
26986                                desc,
26987                                nulls_first,
26988                                collation,
26989                            });
26990                            if matches!(self.peek(), Token::Comma) {
26991                                self.advance();
26992                            } else {
26993                                break;
26994                            }
26995                        }
26996                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
26997                        // follow the ORDER BY inside GROUP_CONCAT.
26998                        if self.consume_group_concat_separator(&mut args)? {
26999                            saw_separator = true;
27000                        }
27001                        if !matches!(self.peek(), Token::RParen) {
27002                            return Err(self.err(format!(
27003                                "expected ')' after aggregate ORDER BY, got {:?}",
27004                                self.peek()
27005                            )));
27006                        }
27007                        break;
27008                    }
27009                    // v7.39 (round 354, M12) — …or directly after the
27010                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
27011                    // own spelling of what PG passes as string_agg's second
27012                    // argument; it was a parse error, so every MySQL query
27013                    // that names its own separator failed outright.
27014                    if self.consume_group_concat_separator(&mut args)? {
27015                        saw_separator = true;
27016                        break;
27017                    }
27018                    match self.peek() {
27019                        Token::Comma => {
27020                            self.advance();
27021                        }
27022                        Token::RParen => break,
27023                        other => {
27024                            return Err(self.err(format!(
27025                                "expected ',' or ')' in function args, got {other:?}"
27026                            )));
27027                        }
27028                    }
27029                }
27030            }
27031            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
27032            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
27033            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
27034            // meaning a separator — that is what the explicit SEPARATOR
27035            // tail is for. Fold them into one `concat(...)` so the
27036            // aggregate keeps its single value argument.
27037            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
27038                let values = args.len() - usize::from(saw_separator);
27039                if values > 1 {
27040                    let sep_arg = if saw_separator { args.pop() } else { None };
27041                    let folded = Expr::FunctionCall {
27042                        name: "concat".to_string(),
27043                        args: core::mem::take(&mut args),
27044                    };
27045                    args.push(folded);
27046                    if let Some(sep) = sep_arg {
27047                        args.push(sep);
27048                    }
27049                }
27050            }
27051            self.advance(); // consume ')'
27052            // v7.39 (read01 round 77) — named arguments are NOT reordered here
27053            // any more. The parser has no catalog, so it could only ever resolve
27054            // the handful of `make_*` builtins whose parameter names were baked
27055            // into a table right here — every user function got
27056            // "does not support named arguments", though the catalog has been
27057            // storing its parameter names all along. Reordering happens in eval,
27058            // in one place, for builtins and user functions alike.
27059            // v7.32 (round-29) — ordered-set aggregate tail
27060            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
27061            // (percentile_cont / percentile_disc / mode). The sort spec
27062            // lands in the same `order_by` slot a decorated aggregate
27063            // uses; the executor dispatches on the function name. WITHIN
27064            // GROUP and an intra-argument ORDER BY are mutually
27065            // exclusive (PG rejects both).
27066            let within_group_order = self.parse_within_group_clause()?;
27067            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
27068                return Err(self.err(
27069                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
27070                        .into(),
27071                ));
27072            }
27073            let within_group_seen = !within_group_order.is_empty();
27074            let agg_order_by = if within_group_order.is_empty() {
27075                agg_order_by
27076            } else {
27077                within_group_order
27078            };
27079            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
27080            let filter = self.parse_filter_clause()?;
27081            // v4.12: window-function tail — `name(args) OVER (...)`.
27082            // Promotes the just-parsed FunctionCall into a
27083            // WindowFunction node carrying partition + order.
27084            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
27085            // / `RESPECT NULLS OVER (...)` between the closing paren
27086            // and `OVER`.
27087            let null_treatment = self.parse_null_treatment_modifier();
27088            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
27089                && s.eq_ignore_ascii_case("over")
27090            {
27091                self.advance();
27092                // v7.39 (round 230) — PG implements neither modifier for a
27093                // windowed call and says so (0A000). Both used to be parsed
27094                // and then silently dropped here, so `count(DISTINCT v)
27095                // OVER (…)` quietly answered the non-distinct count.
27096                if agg_distinct {
27097                    return Err(
27098                        self.err("DISTINCT is not implemented for window functions".to_string())
27099                    );
27100                }
27101                if !agg_order_by.is_empty() {
27102                    // PG separates the two shapes that land here: a
27103                    // WITHIN GROUP call is an ordered-set aggregate and gets
27104                    // its own message naming the aggregate; a plain
27105                    // `agg(x ORDER BY y)` gets the generic one.
27106                    let msg = if within_group_seen {
27107                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
27108                    } else {
27109                        "aggregate ORDER BY is not implemented for window functions".to_string()
27110                    };
27111                    return Err(self.err(msg));
27112                }
27113                let (partition_by, order_by, frame) = self.parse_over_clause()?;
27114                return Ok(Expr::WindowFunction {
27115                    name: first,
27116                    args,
27117                    partition_by,
27118                    order_by,
27119                    frame,
27120                    null_treatment,
27121                    filter,
27122                });
27123            }
27124            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
27125                return Ok(Expr::AggregateOrdered {
27126                    call: Box::new(Expr::FunctionCall { name: first, args }),
27127                    order_by: agg_order_by,
27128                    distinct: agg_distinct,
27129                    filter,
27130                });
27131            }
27132            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
27133            // over TIMESTAMPTZ and has no timestamp overload, so a
27134            // timestamp argument is coerced on the way in and the answer
27135            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
27136            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
27137            // zone`. SPG answered `timestamp without time zone`, dropping
27138            // the offset from every rendering.
27139            //
27140            // Writing the coercion PG performs makes the existing
27141            // argument-driven typing (the one `date_trunc` uses) reach the
27142            // right answer, rather than teaching the type layer a second
27143            // rule. MySQL's DATE_ADD is a different function that returns
27144            // DATE or DATETIME, so this is PG-dialect only.
27145            //
27146            // Out-of-line because this sits on the RECURSIVE descent
27147            // frame: an inline block with locals here costs every nesting
27148            // level, and the suite's deep-nesting sentinel overflowed the
27149            // 512 KiB parser stack the moment one was added (round 430's
27150            // lesson, in the same shape).
27151            if !self.mysql_dialect {
27152                lift_date_add_arg_to_timestamptz(&first, &mut args);
27153            }
27154            return Ok(Expr::FunctionCall { name: first, args });
27155        }
27156        // v7.9.20 — SQL-standard parenless keyword expressions
27157        // (PG treats these as functions called without parens).
27158        // Resolve to a synthetic FunctionCall so the engine's
27159        // eval path reuses the existing function-call routing.
27160        // mailrs G3.
27161        let lc = first.to_ascii_lowercase();
27162        if matches!(
27163            lc.as_str(),
27164            "current_date"
27165                | "current_time"
27166                | "current_timestamp"
27167                | "localtimestamp"
27168                | "localtime"
27169                // v7.37.17 (17.6 siblings) — session-identity SQL-
27170                // standard parenless keywords. current_user /
27171                // session_user / user were already caught by the
27172                // pgwire canned-response shortcut but bare-select
27173                // in the embedded engine went through Expr::Column
27174                // and errored. Adding them here so the parser
27175                // resolves to a synthetic FunctionCall that reuses
27176                // the existing eval/functions.rs dispatch.
27177                | "current_user"
27178                | "session_user"
27179                | "current_role"
27180                | "current_catalog"
27181                | "current_schema"
27182                | "current_database"
27183                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
27184                | "system_user"
27185        ) {
27186            return Ok(Expr::FunctionCall {
27187                name: lc,
27188                args: Vec::new(),
27189            });
27190        }
27191        Ok(Expr::Column(ColumnName {
27192            qualifier: None,
27193            name: first,
27194        }))
27195    }
27196}
27197
27198/// v7.39 (round 522) — write the coercion PG's `date_add` /
27199/// `date_subtract` signature performs.
27200///
27201/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
27202/// timestamp argument is cast on the way in and the answer is
27203/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
27204/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
27205/// `timestamp without time zone`, dropping the offset from every
27206/// rendering of the result.
27207///
27208/// Writing the cast the signature implies lets the existing
27209/// argument-driven typing (the one `date_trunc` uses) reach the right
27210/// answer instead of teaching the type layer a second rule. MySQL's
27211/// DATE_ADD is a different function returning DATE or DATETIME, so the
27212/// caller applies this in PG dialect only.
27213///
27214/// A free function, and not a block at the call site, because the caller
27215/// is on the recursive-descent frame chain.
27216#[inline(never)]
27217fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
27218    if args.len() != 2
27219        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
27220    {
27221        return;
27222    }
27223    let base = args.remove(0);
27224    args.insert(
27225        0,
27226        Expr::Cast {
27227            expr: Box::new(base),
27228            target: CastTarget::Timestamptz,
27229        },
27230    );
27231}
27232
27233/// v6.8.2 — walk an expression tree and return the first column
27234/// reference's bare name. Used by `parse_create_index_stmt_after_create`
27235/// to derive `CreateIndexStatement.column` from an expression
27236/// key (so downstream planner code resolving a primary column
27237/// position keeps working with expression indexes). Returns
27238/// `None` when the expression has no column ref at all — caller
27239/// surfaces that as a parse error.
27240fn extract_first_column(expr: &Expr) -> Option<String> {
27241    match expr {
27242        Expr::Column(cn) => Some(cn.name.clone()),
27243        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
27244        Expr::Binary { lhs, rhs, .. } => {
27245            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
27246        }
27247        Expr::Unary { expr: e, .. } => extract_first_column(e),
27248        // v7.39 (read01 round 93) — a cast wraps its operand: a common
27249        // expression-index key is `lower(col::text)`, where the column
27250        // sits under the `::text` cast inside the function arg. Without
27251        // descending here the key was rejected as "references no column".
27252        Expr::Cast { expr: e, .. } => extract_first_column(e),
27253        // v7.39.2 — and a COLLATE wraps its operand the same way.
27254        // `CREATE INDEX rc ON t (c COLLATE "C" DESC)` stopped naming a
27255        // column the moment the clause became a node instead of being
27256        // absorbed, and the key was rejected as referencing none. This
27257        // is the shape the wildcard below silently produces, which is
27258        // why it is spelled out.
27259        Expr::Collate { expr: e, .. } => extract_first_column(e),
27260        _ => None,
27261    }
27262}
27263
27264fn maybe_not(expr: Expr, negated: bool) -> Expr {
27265    if negated {
27266        Expr::Unary {
27267            op: UnOp::Not,
27268            expr: Box::new(expr),
27269        }
27270    } else {
27271        expr
27272    }
27273}
27274
27275/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
27276/// things in the two dialects, and SPG read all three PG's way:
27277///
27278/// | token | PG (and SPG) | MySQL, measured |
27279/// |---|---|---|
27280/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
27281/// | `&&` | inet / array overlap | **AND** |
27282/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
27283///
27284/// `1 || 0` answering the string '10' on a MySQL session is a wrong
27285/// answer with no error, which is why they are routed here rather than
27286/// left to the shared table.
27287impl Parser {
27288    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
27289        if self.mysql_dialect {
27290            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
27291            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
27292            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
27293            if let Token::Ident(w) = tok
27294                && w.eq_ignore_ascii_case("div")
27295            {
27296                return Some((BinOp::IntDiv, 8));
27297            }
27298            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
27299            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
27300            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
27301            // there sits in operand position, not infix).
27302            if let Token::Ident(w) = tok
27303                && w.eq_ignore_ascii_case("mod")
27304            {
27305                return Some((BinOp::Mod, 8));
27306            }
27307            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
27308            // plain ident to the lexer. Its precedence sits between OR (1)
27309            // and AND (3) — hence rung 2, the slot freed by moving AND up.
27310            if let Token::Ident(w) = tok
27311                && w.eq_ignore_ascii_case("xor")
27312            {
27313                return Some((BinOp::LogicalXor, 2));
27314            }
27315            match tok {
27316                Token::Concat => return Some((BinOp::Or, 1)),
27317                // MySQL's `&&` is logical AND, sharing AND's rung (3).
27318                Token::InetOverlap => return Some((BinOp::And, 3)),
27319                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
27320                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
27321                _ => {}
27322            }
27323        }
27324        binop_from(tok)
27325    }
27326}
27327
27328// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
27329// (which sits strictly between OR and AND), every level from AND upward was
27330// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
27331// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
27332// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
27333// the *relative* order of every PG operator is unchanged by the shift.
27334fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
27335    let pair = match tok {
27336        Token::Or => (BinOp::Or, 1),
27337        Token::And => (BinOp::And, 3),
27338        Token::Eq => (BinOp::Eq, 5),
27339        Token::NotEq => (BinOp::NotEq, 5),
27340        Token::Lt => (BinOp::Lt, 5),
27341        Token::LtEq => (BinOp::LtEq, 5),
27342        Token::Gt => (BinOp::Gt, 5),
27343        Token::GtEq => (BinOp::GtEq, 5),
27344        // pgvector distance ops all sit on the same rung — tighter than
27345        // comparisons (5) so `col <-> v < threshold` parses correctly.
27346        Token::L2Distance => (BinOp::L2Distance, 6),
27347        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
27348        // comparison rung.
27349        Token::GeomParallel => (BinOp::GeomParallel, 5),
27350        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
27351        // comparison rung.
27352        Token::OverLeft => (BinOp::OverLeft, 5),
27353        Token::OverRight => (BinOp::OverRight, 5),
27354        Token::GeomPerp => (BinOp::GeomPerp, 5),
27355        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
27356        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
27357        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
27358        Token::InnerProduct => (BinOp::InnerProduct, 6),
27359        Token::CosineDistance => (BinOp::CosineDistance, 6),
27360        Token::Plus => (BinOp::Add, 7),
27361        Token::Minus => (BinOp::Sub, 7),
27362        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
27363        // binds every "other" operator (`||`, `|`, `&`, `#`, the
27364        // pgvector distances above) BETWEEN additive (7) and the
27365        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
27366        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
27367        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
27368        // ("matches PG conceptually" — the round-753 audit measured it
27369        // false; the old rung errored on `'a' || 1 + 1` with
27370        // `text + integer`). Same-level chains left-fold, as PG does.
27371        Token::Concat => (BinOp::Concat, 6),
27372        Token::Pipe => (BinOp::BitOr, 6),
27373        Token::Amp => (BinOp::BitAnd, 6),
27374        Token::Star => (BinOp::Mul, 8),
27375        Token::Slash => (BinOp::Div, 8),
27376        Token::Percent => (BinOp::Mod, 8),
27377        // v4.14: JSON path ops bind tighter than comparisons (5)
27378        // and additive (7) so `doc->'k' = 'v'` parses correctly.
27379        // Same rung as the multiplicative ops.
27380        Token::JsonGet => (BinOp::JsonGet, 8),
27381        Token::JsonGetText => (BinOp::JsonGetText, 8),
27382        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
27383        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
27384        Token::JsonContains => (BinOp::JsonContains, 8),
27385        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
27386        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
27387        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
27388        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
27389        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
27390        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
27391        // v7.12.2 — `@@` binds at the comparison rung (looser than
27392        // arithmetic, tighter than AND / OR). PG places `@@` at
27393        // the same precedence as `=` / `<`, so we follow.
27394        Token::TsMatch => (BinOp::TsMatch, 5),
27395        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
27396        // PG places these at the comparison rung (same level as `=`),
27397        // so we follow.
27398        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
27399        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
27400        Token::InetContains => (BinOp::InetContains, 5),
27401        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
27402        Token::InetOverlap => (BinOp::InetOverlap, 5),
27403        // v7.39 (round 508) — the geometric and pattern-order predicates
27404        // ride the comparison rung, as every other predicate does.
27405        Token::Intersects => (BinOp::Intersects, 5),
27406        Token::IsBelow => (BinOp::IsBelow, 5),
27407        Token::IsAbove => (BinOp::IsAbove, 5),
27408        Token::PatternLt => (BinOp::PatternLt, 5),
27409        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
27410        Token::PatternGt => (BinOp::PatternGt, 5),
27411        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
27412        // `@@@` is the old spelling of `@@` and means exactly it.
27413        Token::TsMatchOld => (BinOp::TsMatch, 5),
27414        _ => return None,
27415    };
27416    Some(pair)
27417}
27418
27419#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27420// `as f32` here is intentional: vector elements widen / narrow into f32 on
27421// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
27422// past ~15 decimal digits — both are acceptable for a fixed-precision
27423// pgvector column.
27424/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
27425/// implicit table alias and break trailing clauses. WITH lands
27426/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
27427/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
27428/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
27429/// / VALUES / FOR / LATERAL — all of which would otherwise be
27430/// silently swallowed by `parse_optional_alias`.
27431fn is_alias_stopword(s: &str) -> bool {
27432    matches!(
27433        s.to_ascii_lowercase().as_str(),
27434        "with"
27435            | "on"
27436            | "where"
27437            | "having"
27438            | "group"
27439            | "order"
27440            | "limit"
27441            | "offset"
27442            | "union"
27443            | "except"
27444            | "intersect"
27445            | "returning"
27446            | "set"
27447            | "values"
27448            | "for"
27449            | "window"
27450            | "tablesample"
27451            | "lateral"
27452            | "left"
27453            | "right"
27454            | "inner"
27455            | "outer"
27456            | "full"
27457            | "cross"
27458            | "join"
27459            | "natural"
27460            | "using"
27461            | "fetch"
27462    )
27463}
27464
27465fn extract_numeric_literal(e: &Expr) -> Option<f32> {
27466    match e {
27467        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
27468        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
27469        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
27470        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
27471        // so scale the divisor by hand instead of `f32::powi`.)
27472        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27473            let mut div = 1.0f32;
27474            for _ in 0..*scale {
27475                div *= 10.0;
27476            }
27477            Some(*unscaled as f32 / div)
27478        }
27479        Expr::Unary {
27480            op: UnOp::Neg,
27481            expr,
27482        } => extract_numeric_literal(expr).map(|x| -x),
27483        _ => None,
27484    }
27485}
27486
27487/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
27488/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
27489/// negative. Returns `None` if any pair fails to parse or no pair is found.
27490///
27491/// Recognised units (case-insensitive, optional trailing `s`):
27492/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
27493/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
27494/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
27495/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
27496/// (PG-canonical: DST and month-boundary semantics depend on this).
27497/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
27498/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
27499/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
27500#[allow(clippy::cast_possible_truncation)]
27501fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
27502    let mut months: i64 = 0;
27503    let mut days: i64 = 0;
27504    let mut micros: i64 = 0;
27505    let mut in_time = false;
27506    let mut num = alloc::string::String::new();
27507    for ch in rest.chars() {
27508        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
27509            num.push(ch);
27510            continue;
27511        }
27512        if ch == 'T' || ch == 't' {
27513            if !num.is_empty() {
27514                return None;
27515            }
27516            in_time = true;
27517            continue;
27518        }
27519        let n: f64 = num.parse().ok()?;
27520        num.clear();
27521        match (ch, in_time) {
27522            ('Y' | 'y', false) => months += (n * 12.0) as i64,
27523            ('M', false) => months += n as i64,
27524            ('W' | 'w', false) => days += (n * 7.0) as i64,
27525            ('D' | 'd', false) => days += n as i64,
27526            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
27527            ('M', true) => micros += (n * 60_000_000.0) as i64,
27528            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
27529            _ => return None,
27530        }
27531    }
27532    if !num.is_empty() {
27533        return None;
27534    }
27535    Some((
27536        i32::try_from(months).ok()?,
27537        i32::try_from(days).ok()?,
27538        micros,
27539    ))
27540}
27541
27542/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
27543/// leading `-` negates the whole value). Rejects date-like strings.
27544fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
27545    let (neg, body) = match s.strip_prefix('-') {
27546        Some(b) => (true, b),
27547        None => (false, s),
27548    };
27549    let (y, m) = body.split_once('-')?;
27550    let years: i32 = y.parse().ok()?;
27551    let mons: i32 = m.parse().ok()?;
27552    if years < 0 || mons < 0 {
27553        return None;
27554    }
27555    let total = years.checked_mul(12)?.checked_add(mons)?;
27556    Some((if neg { -total } else { total }, 0, 0))
27557}
27558
27559/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
27560/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
27561fn parse_interval_clock(tok: &str) -> Option<i64> {
27562    let (neg, body) = match tok.strip_prefix('-') {
27563        Some(r) => (true, r),
27564        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
27565    };
27566    let mut it = body.split(':');
27567    let h: i64 = it.next()?.parse().ok()?;
27568    let m: i64 = it.next()?.parse().ok()?;
27569    let s_tok = it.next().unwrap_or("0");
27570    if it.next().is_some() {
27571        return None;
27572    }
27573    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
27574        let sec: i64 = sec.parse().ok()?;
27575        let mut f = alloc::string::String::from(frac);
27576        while f.len() < 6 {
27577            f.push('0');
27578        }
27579        f.truncate(6);
27580        let fus: i64 = f.parse().ok()?;
27581        sec.checked_mul(1_000_000)?.checked_add(fus)?
27582    } else {
27583        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
27584    };
27585    let total = h
27586        .checked_mul(3_600_000_000)?
27587        .checked_add(m.checked_mul(60_000_000)?)?
27588        .checked_add(sec_us)?;
27589    Some(if neg { -total } else { total })
27590}
27591
27592/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
27593/// every spelling PG accepts (measured against live PG18.4, not guessed):
27594/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
27595/// Before this, the unit table matched long names only, with an ad-hoc
27596/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
27597/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
27598/// INTERVAL", and it had also grown arms for the debris that stripping leaves
27599/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
27600/// fractional) both read from this one table now.
27601fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
27602    let u = raw.to_ascii_lowercase();
27603    Some(match u.as_str() {
27604        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
27605            "microsecond"
27606        }
27607        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
27608            "millisecond"
27609        }
27610        "second" | "seconds" | "sec" | "secs" | "s" => "second",
27611        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
27612        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
27613        "day" | "days" | "d" => "day",
27614        "week" | "weeks" | "w" => "week",
27615        "month" | "months" | "mon" | "mons" => "month",
27616        "year" | "years" | "yr" | "yrs" | "y" => "year",
27617        "decade" | "decades" | "dec" | "decs" => "decade",
27618        "century" | "centuries" | "cent" | "c" => "century",
27619        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
27620        _ => return None,
27621    })
27622}
27623
27624/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
27625/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
27626#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27627pub(crate) enum IntervalField {
27628    Year,
27629    Month,
27630    Day,
27631    Hour,
27632    Minute,
27633    Second,
27634}
27635
27636/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
27637/// spellings aren't standard for the qualifier position, so only the singular
27638/// forms are accepted.
27639/// v7.39 (round 350, M7) — MySQL's interval units, measured against
27640/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
27641/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
27642/// take a `'1 2'` style literal — are not read here; they stay a parse
27643/// error rather than being silently misread.)
27644/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
27645///
27646/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
27647/// to do with a `@@` engine setting, and an unset one reads NULL rather
27648/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
27649/// were the same node and `SELECT @x` answered "Unknown system variable".)
27650/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
27651/// not see a session override — measured, after `SET autocommit=0`,
27652/// `@@global.autocommit` is still 1.
27653///
27654/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
27655/// the parser's nesting budget is tuned against, and building these
27656/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
27657/// wall `parse_left_right_atom` and friends were factored out for).
27658#[inline(never)]
27659fn variable_ref_atom(raw: &str) -> Expr {
27660    let user_var = !raw.starts_with("@@");
27661    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
27662    Expr::FunctionCall {
27663        name: String::from(if user_var {
27664            "__spg_user_var"
27665        } else {
27666            "__spg_session_var"
27667        }),
27668        args: alloc::vec![Expr::Literal(Literal::String(bare))],
27669    }
27670}
27671
27672fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
27673    let Token::Ident(s) = tok else { return None };
27674    Some(match () {
27675        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
27676        () if s.eq_ignore_ascii_case("second") => "second",
27677        () if s.eq_ignore_ascii_case("minute") => "minute",
27678        () if s.eq_ignore_ascii_case("hour") => "hour",
27679        () if s.eq_ignore_ascii_case("day") => "day",
27680        () if s.eq_ignore_ascii_case("week") => "week",
27681        () if s.eq_ignore_ascii_case("month") => "month",
27682        () if s.eq_ignore_ascii_case("quarter") => "quarter",
27683        () if s.eq_ignore_ascii_case("year") => "year",
27684        () => return None,
27685    })
27686}
27687
27688/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
27689/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
27690/// which constructs the value at run time. Only the slot the unit names
27691/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
27692/// slot the builtin has (months and fractional seconds respectively).
27693fn make_interval_call(qty: Expr, unit: &str) -> Expr {
27694    let zero = || Expr::Literal(Literal::Integer(0));
27695    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
27696        lhs: alloc::boxed::Box::new(qty.clone()),
27697        op,
27698        rhs: alloc::boxed::Box::new(by),
27699    };
27700    // (years, months, weeks, days, hours, mins, secs)
27701    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
27702    match unit {
27703        "year" => args[0] = qty,
27704        "quarter" => {
27705            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
27706        }
27707        "month" => args[1] = qty,
27708        "week" => args[2] = qty,
27709        "day" => args[3] = qty,
27710        "hour" => args[4] = qty,
27711        "minute" => args[5] = qty,
27712        "second" => args[6] = qty,
27713        // The builtin's seconds slot takes a fraction, so microseconds ride
27714        // it scaled down; the divisor is a NUMERIC literal so the division
27715        // stays exact rather than going through a float.
27716        "microsecond" => {
27717            args[6] = scaled(
27718                crate::ast::BinOp::Div,
27719                Expr::Literal(Literal::Numeric {
27720                    unscaled: 1_000_000,
27721                    scale: 0,
27722                }),
27723            );
27724        }
27725        _ => args[3] = qty,
27726    }
27727    Expr::FunctionCall {
27728        name: alloc::string::String::from("make_interval"),
27729        args,
27730    }
27731}
27732
27733/// `(count, unit)` → `(months, days, micros)`.
27734fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
27735    let n: i64 = count.trim().parse().ok()?;
27736    Some(match unit {
27737        "microsecond" => (0, 0, n),
27738        "second" => (0, 0, n.checked_mul(1_000_000)?),
27739        "minute" => (0, 0, n.checked_mul(60_000_000)?),
27740        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
27741        "day" => (0, i32::try_from(n).ok()?, 0),
27742        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
27743        "month" => (i32::try_from(n).ok()?, 0, 0),
27744        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
27745        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
27746        _ => return None,
27747    })
27748}
27749
27750fn interval_field_of(tok: &Token) -> Option<IntervalField> {
27751    let Token::Ident(s) = tok else { return None };
27752    Some(match () {
27753        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
27754        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
27755        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
27756        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
27757        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
27758        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
27759        () => return None,
27760    })
27761}
27762
27763/// v7.39 (read01 round 102) — interpret an interval literal under a field
27764/// qualifier. Returns `(months, days, micros)`.
27765///
27766/// * A single field applied to a bare number sets which unit the number means,
27767///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
27768///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
27769/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
27770/// * Every other range, and any literal a single field can't read as a plain
27771///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
27772///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
27773///   like PG, and the qualifier there only bounds precision.
27774fn interpret_qualified_interval(
27775    text: &str,
27776    (f1, f2): (IntervalField, Option<IntervalField>),
27777) -> Option<(i32, i32, i64)> {
27778    if let Some(f2) = f2 {
27779        if f1 == IntervalField::Year && f2 == IntervalField::Month {
27780            if let Some(m) = parse_year_month_literal(text) {
27781                return Some((m, 0, 0));
27782            }
27783        }
27784        return parse_interval_text(text);
27785    }
27786    // Single field: reinterpret a bare number; otherwise the default parse.
27787    let trimmed = text.trim();
27788    if let Ok(val) = trimmed.parse::<f64>() {
27789        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
27790        #[allow(clippy::cast_possible_truncation)]
27791        let whole = val as i64;
27792        #[allow(clippy::cast_possible_truncation)]
27793        let secs_micros = {
27794            let m = val * 1_000_000.0;
27795            if m >= 0.0 {
27796                (m + 0.5) as i64
27797            } else {
27798                (m - 0.5) as i64
27799            }
27800        };
27801        return Some(match f1 {
27802            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
27803            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
27804            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
27805            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
27806            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
27807            IntervalField::Second => (0, 0, secs_micros),
27808        });
27809    }
27810    parse_interval_text(text)
27811}
27812
27813/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
27814fn parse_year_month_literal(text: &str) -> Option<i32> {
27815    let t = text.trim();
27816    let (neg, body) = match t.strip_prefix('-') {
27817        Some(r) => (true, r),
27818        None => (false, t.strip_prefix('+').unwrap_or(t)),
27819    };
27820    let mut it = body.split('-');
27821    let years: i32 = it.next()?.trim().parse().ok()?;
27822    let months: i32 = match it.next() {
27823        Some(m) => m.trim().parse().ok()?,
27824        None => 0,
27825    };
27826    if it.next().is_some() {
27827        return None;
27828    }
27829    let total = years.checked_mul(12)?.checked_add(months)?;
27830    Some(if neg { -total } else { total })
27831}
27832
27833pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
27834    // v7.38.19 — the two infinities, answered as the three extreme
27835    // fields PostgreSQL itself puts on the wire for them:
27836    //
27837    //   COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
27838    //     … 7fffffffffffffff 7fffffff 7fffffff
27839    //
27840    // So no caller has to know the spelling — every one of them already
27841    // reads the three numbers, and `IntervalKind::from_fields` names
27842    // what they mean.
27843    //
27844    // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
27845    // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
27846    // infinity. Interval takes the full word, in any case.
27847    {
27848        let word = s.trim();
27849        let word = word.strip_prefix('@').map_or(word, str::trim);
27850        let (neg, body) = match word.strip_prefix('-') {
27851            Some(rest) => (true, rest.trim_start()),
27852            None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
27853        };
27854        if body.eq_ignore_ascii_case("infinity") {
27855            return Some(if neg {
27856                (i32::MIN, i32::MIN, i64::MIN)
27857            } else {
27858                (i32::MAX, i32::MAX, i64::MAX)
27859            });
27860        }
27861    }
27862    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
27863    // `@` is decorative; a trailing `ago` negates the whole interval.
27864    let mut trimmed = s.trim();
27865    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
27866    let mut negate = false;
27867    if let Some(rest) = trimmed
27868        .strip_suffix("ago")
27869        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
27870    {
27871        negate = true;
27872        trimmed = rest.trim();
27873    }
27874    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
27875        let (mo, d, us) = v?;
27876        if negate {
27877            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
27878        } else {
27879            Some((mo, d, us))
27880        }
27881    };
27882    let s = trimmed;
27883    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
27884    // are single tokens, not the `<n> <unit>` pair form handled below.
27885    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
27886        return finish(parse_iso8601_interval(rest));
27887    }
27888    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
27889        if let Some(iv) = parse_year_month_interval(trimmed) {
27890            return finish(Some(iv));
27891        }
27892    }
27893    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
27894    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
27895    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
27896    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
27897        if let Ok(n) = trimmed.parse::<i64>() {
27898            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
27899        }
27900        if let Ok(f) = trimmed.parse::<f64>() {
27901            if f.is_finite() {
27902                #[allow(clippy::cast_possible_truncation)]
27903                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
27904            }
27905        }
27906    }
27907    // v7.39 (round 243) — PG accepts the number and unit run together
27908    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
27909    // the `<n> <unit>` pair loop below sees them as two.
27910    let raw_parts: Vec<&str> = s.split_whitespace().collect();
27911    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
27912    for p in raw_parts {
27913        let boundary = p
27914            .char_indices()
27915            .find(|(i, c)| {
27916                *i > 0
27917                    && c.is_ascii_alphabetic()
27918                    && p[..*i]
27919                        .chars()
27920                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
27921                    && p[..*i].chars().any(|d| d.is_ascii_digit())
27922            })
27923            .map(|(i, _)| i);
27924        match boundary {
27925            Some(i) => {
27926                parts.push(&p[..i]);
27927                parts.push(&p[i..]);
27928            }
27929            None => parts.push(p),
27930        }
27931    }
27932    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
27933    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
27934    // remains is the `<n> <unit>` pair form handled below.
27935    let mut clock_us: i64 = 0;
27936    let mut had_clock = false;
27937    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
27938        clock_us = parse_interval_clock(parts[pos])?;
27939        parts.remove(pos);
27940        had_clock = true;
27941    }
27942    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
27943    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
27944    let mut lone_days: i32 = 0;
27945    if had_clock && parts.len() == 1 {
27946        if let Ok(n) = parts[0].parse::<i64>() {
27947            lone_days = i32::try_from(n).ok()?;
27948            parts.clear();
27949        }
27950    }
27951    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
27952        return None;
27953    }
27954    let mut months: i32 = 0;
27955    let mut days: i32 = lone_days;
27956    let mut micros: i64 = clock_us;
27957    let mut i = 0;
27958    while i < parts.len() {
27959        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
27960        if let Ok(n) = parts[i].parse::<i64>() {
27961            match unit_stripped {
27962                "microsecond" => micros = micros.checked_add(n)?,
27963                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
27964                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
27965                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
27966                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
27967                "day" => {
27968                    let n32 = i32::try_from(n).ok()?;
27969                    days = days.checked_add(n32)?;
27970                }
27971                "week" => {
27972                    let n32 = i32::try_from(n).ok()?;
27973                    days = days.checked_add(n32.checked_mul(7)?)?;
27974                }
27975                "month" => {
27976                    let n32 = i32::try_from(n).ok()?;
27977                    months = months.checked_add(n32)?;
27978                }
27979                "year" => {
27980                    let n32 = i32::try_from(n).ok()?;
27981                    months = months.checked_add(n32.checked_mul(12)?)?;
27982                }
27983                // v7.39 (read01 timestamp.c) — the larger calendar units.
27984                "decade" => {
27985                    let n32 = i32::try_from(n).ok()?;
27986                    months = months.checked_add(n32.checked_mul(120)?)?;
27987                }
27988                "century" => {
27989                    let n32 = i32::try_from(n).ok()?;
27990                    months = months.checked_add(n32.checked_mul(1200)?)?;
27991                }
27992                "millennium" => {
27993                    let n32 = i32::try_from(n).ok()?;
27994                    months = months.checked_add(n32.checked_mul(12000)?)?;
27995                }
27996                _ => return None,
27997            }
27998        } else if let Ok(f) = parts[i].parse::<f64>() {
27999            // Fractional units cascade down to the next-finer field the way
28000            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
28001            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
28002            // no_std: f64 has no trunc/fract/round methods, so do them with
28003            // casts (toward-zero) + explicit round-half-away-from-zero.
28004            #[allow(clippy::cast_possible_truncation)]
28005            fn round_i64(x: f64) -> i64 {
28006                if x >= 0.0 {
28007                    (x + 0.5) as i64
28008                } else {
28009                    (x - 0.5) as i64
28010                }
28011            }
28012            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
28013            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
28014                const DAY_US: f64 = 86_400_000_000.0;
28015                let whole = d as i64; // truncates toward zero
28016                let frac = d - whole as f64;
28017                *days = days.checked_add(i32::try_from(whole).ok()?)?;
28018                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
28019                Some(())
28020            }
28021            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
28022            match unit_stripped {
28023                "microsecond" => micros = micros.checked_add(round_i64(f))?,
28024                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
28025                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
28026                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
28027                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
28028                "day" => add_days_frac(&mut days, &mut micros, f)?,
28029                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
28030                "month" => {
28031                    let whole = f as i64;
28032                    months = months.checked_add(i32::try_from(whole).ok()?)?;
28033                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
28034                }
28035                "year" => {
28036                    let m = f * 12.0;
28037                    let whole = m as i64;
28038                    months = months.checked_add(i32::try_from(whole).ok()?)?;
28039                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
28040                }
28041                _ => return None,
28042            }
28043        } else {
28044            return None;
28045        }
28046        i += 2;
28047    }
28048    finish(Some((months, days, micros)))
28049}
28050
28051/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
28052/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
28053/// `interval` is intentionally absent (handled by its own parser arm).
28054/// Returns `None` for names that aren't sensible as a bare typed literal, so
28055/// the caller falls back to treating the ident as a column reference.
28056fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
28057    Some(match ident {
28058        "date" => CastTarget::Date,
28059        "timestamp" | "datetime" => CastTarget::Timestamp,
28060        "timestamptz" => CastTarget::Timestamptz,
28061        "bool" | "boolean" => CastTarget::Bool,
28062        "int" | "integer" | "int4" => CastTarget::Int,
28063        "bigint" | "int8" => CastTarget::BigInt,
28064        "float8" | "double precision" => CastTarget::Float,
28065        "uuid" => CastTarget::Uuid,
28066        "bytea" => CastTarget::Bytea,
28067        "json" => CastTarget::Json,
28068        "jsonb" => CastTarget::Jsonb,
28069        // Types without a dedicated CastTarget variant flow through the
28070        // generic Named path (engine resolves via column_type_to_data_type).
28071        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
28072        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
28073        | "money" | "bit" | "varbit"
28074        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
28075        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
28076        // Range / multirange types likewise.
28077        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
28078        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
28079        | "datemultirange" | "tsmultirange" | "tstzmultirange"
28080        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
28081        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
28082            CastTarget::Named(alloc::string::String::from(ident))
28083        }
28084        _ => return None,
28085    })
28086}
28087
28088/// v7.12.4 — map a bare type-name identifier (the form that
28089/// appears in a function arg list or RETURNS clause) to a
28090/// [`ColumnTypeName`]. Returns `None` for unknown / extension
28091/// types so the caller can preserve them as
28092/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
28093///
28094/// Subset of the full column-type grammar — we deliberately
28095/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
28096/// here because function-arg types in v7.12.4 are mostly the
28097/// bare form (`text`, `int`, `bytea`, …).
28098/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
28099/// than being `name TYPE`?
28100///
28101/// The multi-word spellings SQL allows for a bare argument type, each
28102/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
28103///
28104/// NOTE this list also exists in `spg-storage`, which computes the
28105/// signature key from the rendered argument text and has to reach the
28106/// same verdict. The two crates are siblings — neither depends on the
28107/// other — and each already carries its own table of type spellings
28108/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
28109/// there), so this follows the structure rather than inventing new
28110/// duplication. Recorded as V49.
28111pub fn is_multiword_type_phrase(phrase: &str) -> bool {
28112    let t = phrase.trim().to_ascii_lowercase();
28113    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
28114    matches!(
28115        base,
28116        "double precision"
28117            | "character varying"
28118            | "bit varying"
28119            | "timestamp with time zone"
28120            | "timestamp without time zone"
28121            | "time with time zone"
28122            | "time without time zone"
28123            | "national character"
28124            | "national character varying"
28125    )
28126}
28127
28128fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
28129    Some(match ident.to_ascii_lowercase().as_str() {
28130        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
28131        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
28132        "bigint" => ColumnTypeName::BigInt,
28133        "float" | "double" => ColumnTypeName::Float,
28134        // v7.39 (round 269) — real is 32-bit.
28135        "real" | "float4" => ColumnTypeName::Real,
28136        "text" => ColumnTypeName::Text,
28137        "bool" | "boolean" => ColumnTypeName::Bool,
28138        "date" => ColumnTypeName::Date,
28139        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
28140        "timestamptz" => ColumnTypeName::Timestamptz,
28141        "json" => ColumnTypeName::Json,
28142        "jsonb" => ColumnTypeName::Jsonb,
28143        "bytea" | "bytes" => ColumnTypeName::Bytes,
28144        "tsvector" => ColumnTypeName::TsVector,
28145        "tsquery" => ColumnTypeName::TsQuery,
28146        "uuid" => ColumnTypeName::Uuid,
28147        "interval" => ColumnTypeName::Interval,
28148        "time" => ColumnTypeName::Time,
28149        "year" => ColumnTypeName::Year,
28150        "timetz" => ColumnTypeName::TimeTz,
28151        "money" => ColumnTypeName::Money,
28152        _ => return None,
28153    })
28154}
28155
28156/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
28157/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
28158///
28159/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
28160/// / embedded SQL land in v7.12.5+):
28161///
28162/// ```text
28163///   body          := [ws] block [ws]
28164///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
28165///   stmt          := assign | return
28166///   assign        := assign_target := expr
28167///   assign_target := ( NEW | OLD ) . ident | ident
28168///   return        := RETURN ( NEW | OLD | NULL | expr )
28169/// ```
28170///
28171/// `expr` is parsed by recursing into the regular `Parser` — so a
28172/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
28173/// NEW.subject || ' ' || NEW.sender)` body shape works without
28174/// the body parser knowing what `to_tsvector` is.
28175///
28176/// Errors here cause the caller to fall back to
28177/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
28178/// successful, but the executor will refuse to invoke the
28179/// function with an "unparseable body" error.
28180/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
28181/// from the crate root as `spg_sql::parse_function_body`.
28182pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
28183    parse_plpgsql_body(body)
28184}
28185
28186fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
28187    // Use the regular lexer on the body text. The trailing
28188    // `END;` may or may not have a semicolon; the lexer treats
28189    // both forms identically.
28190    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
28191        message: alloc::format!("plpgsql body lex error: {e}"),
28192        token_pos: 0,
28193    })?;
28194    let mut parser = Parser::new(tokens);
28195    parser.parse_plpgsql_block()
28196}
28197
28198/// v7.39 (GUC) — the textual body of a SET value, for list joining.
28199fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
28200    match v {
28201        crate::ast::SetValue::String(s)
28202        | crate::ast::SetValue::Ident(s)
28203        | crate::ast::SetValue::Number(s) => s.clone(),
28204        crate::ast::SetValue::Default => "DEFAULT".into(),
28205        crate::ast::SetValue::Null => "NULL".into(),
28206    }
28207}
28208
28209/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
28210/// contains an aggregate call at ITS OWN query level (recursion stops at
28211/// sublink boundaries — a sublink's aggregates belong to the sublink).
28212/// Backs the "aggregate functions are not allowed in a recursive query's
28213/// recursive term" well-formedness check.
28214/// v7.40.0 — is this the name of an aggregate? The same list
28215/// `expr_has_toplevel_aggregate` walks, exposed so the grouping-set
28216/// rewrite can leave an aggregate's ARGUMENT alone.
28217pub(crate) fn is_aggregate_function_name(name: &str) -> bool {
28218    AGG_NAMES.iter().any(|a| name.eq_ignore_ascii_case(a))
28219}
28220
28221const AGG_NAMES: &[&str] = &[
28222    "count",
28223    "sum",
28224    "min",
28225    "max",
28226    "avg",
28227    "string_agg",
28228    "array_agg",
28229    "bool_and",
28230    "bool_or",
28231    "every",
28232    "any_value",
28233    "json_agg",
28234    "jsonb_agg",
28235    "json_object_agg",
28236    "jsonb_object_agg",
28237    "bit_and",
28238    "bit_or",
28239    "bit_xor",
28240    "var_pop",
28241    "var_samp",
28242    "variance",
28243    "std",
28244    "stddev",
28245    "stddev_pop",
28246    "stddev_samp",
28247    "range_agg",
28248    "range_intersect_agg",
28249    "percentile_cont",
28250    "percentile_disc",
28251    "mode",
28252    "corr",
28253    "covar_pop",
28254    "covar_samp",
28255];
28256
28257fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
28258    match e {
28259        Expr::AggregateOrdered { .. } => true,
28260        Expr::FunctionCall { name, args } => {
28261            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
28262                || args.iter().any(expr_has_toplevel_aggregate)
28263        }
28264        Expr::NamedArg { expr, .. }
28265        | Expr::Variadic(expr)
28266        | Expr::Unary { expr, .. }
28267        | Expr::Cast { expr, .. }
28268        | Expr::IsNull { expr, .. }
28269        | Expr::FieldAccess { base: expr, .. }
28270        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
28271        Expr::Binary { lhs, rhs, .. } => {
28272            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
28273        }
28274        Expr::Like { expr, pattern, .. } => {
28275            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
28276        }
28277        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
28278        Expr::InList { expr, list, .. } => {
28279            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
28280        }
28281        Expr::ArraySubscript { target, index } => {
28282            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
28283        }
28284        Expr::ArraySlice { target, lo, hi } => {
28285            expr_has_toplevel_aggregate(target)
28286                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
28287                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
28288        }
28289        Expr::AnyAll { expr, array, .. } => {
28290            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
28291        }
28292        Expr::Case {
28293            operand,
28294            branches,
28295            else_branch,
28296        } => {
28297            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
28298                || branches
28299                    .iter()
28300                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
28301                || else_branch
28302                    .as_deref()
28303                    .is_some_and(expr_has_toplevel_aggregate)
28304        }
28305        // The outer-level operands of a sublink can aggregate; the sublink's
28306        // own body cannot leak its aggregates up here.
28307        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
28308        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
28309            row.iter().any(expr_has_toplevel_aggregate)
28310        }
28311        _ => false,
28312    }
28313}
28314
28315/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
28316/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
28317/// named table anywhere in its subtree. A plain FROM derived table is NOT a
28318/// sublink and is legal in a recursive term, so it is not walked here.
28319fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
28320    let mut exprs: Vec<&Expr> = Vec::new();
28321    for it in &s.items {
28322        if let crate::ast::SelectItem::Expr { expr, .. } = it {
28323            exprs.push(expr);
28324        }
28325    }
28326    if let Some(w) = &s.where_ {
28327        exprs.push(w);
28328    }
28329    if let Some(h) = &s.having {
28330        exprs.push(h);
28331    }
28332    if let Some(g) = &s.group_by {
28333        exprs.extend(g.iter());
28334    }
28335    if let Some(from) = &s.from {
28336        for j in &from.joins {
28337            if let Some(on) = &j.on {
28338                exprs.push(on);
28339            }
28340        }
28341    }
28342    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
28343}
28344
28345/// Does this expression contain a sublink whose subquery mentions `name`?
28346fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
28347    match e {
28348        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
28349        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
28350        Expr::InSubquery { expr, subquery, .. } => {
28351            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
28352        }
28353        Expr::RowInSubquery { row, subquery, .. } => {
28354            row.iter().any(|x| expr_sublink_mentions(x, name))
28355                || select_mentions_table(subquery, name)
28356        }
28357        Expr::RowCmpSubquery { row, subquery, .. } => {
28358            row.iter().any(|x| expr_sublink_mentions(x, name))
28359                || select_mentions_table(subquery, name)
28360        }
28361        Expr::NamedArg { expr, .. }
28362        | Expr::Variadic(expr)
28363        | Expr::Unary { expr, .. }
28364        | Expr::Cast { expr, .. }
28365        | Expr::IsNull { expr, .. }
28366        | Expr::FieldAccess { base: expr, .. }
28367        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
28368        Expr::Binary { lhs, rhs, .. } => {
28369            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
28370        }
28371        Expr::Like { expr, pattern, .. } => {
28372            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
28373        }
28374        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
28375            args.iter().any(|x| expr_sublink_mentions(x, name))
28376        }
28377        Expr::InList { expr, list, .. } => {
28378            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
28379        }
28380        Expr::ArraySubscript { target, index } => {
28381            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
28382        }
28383        Expr::ArraySlice { target, lo, hi } => {
28384            expr_sublink_mentions(target, name)
28385                || lo
28386                    .as_deref()
28387                    .is_some_and(|x| expr_sublink_mentions(x, name))
28388                || hi
28389                    .as_deref()
28390                    .is_some_and(|x| expr_sublink_mentions(x, name))
28391        }
28392        Expr::AnyAll { expr, array, .. } => {
28393            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
28394        }
28395        Expr::Case {
28396            operand,
28397            branches,
28398            else_branch,
28399        } => {
28400            operand
28401                .as_deref()
28402                .is_some_and(|x| expr_sublink_mentions(x, name))
28403                || branches
28404                    .iter()
28405                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
28406                || else_branch
28407                    .as_deref()
28408                    .is_some_and(|x| expr_sublink_mentions(x, name))
28409        }
28410        _ => false,
28411    }
28412}
28413
28414/// Does this SELECT (in full — FROM tables, derived tables, its own
28415/// sublinks, and union arms) mention the named table?
28416fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
28417    if let Some(from) = &s.from {
28418        if from.primary.name.eq_ignore_ascii_case(name) {
28419            return true;
28420        }
28421        if let Some(sub) = &from.primary.lateral_subquery
28422            && select_mentions_table(sub, name)
28423        {
28424            return true;
28425        }
28426        for j in &from.joins {
28427            if j.table.name.eq_ignore_ascii_case(name) {
28428                return true;
28429            }
28430            if let Some(sub) = &j.table.lateral_subquery
28431                && select_mentions_table(sub, name)
28432            {
28433                return true;
28434            }
28435        }
28436    }
28437    if select_has_self_ref_in_sublink(s, name) {
28438        return true;
28439    }
28440    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
28441}
28442
28443/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
28444/// row count, the way PG evaluates one before applying it.
28445///
28446/// `None` = not a constant (a column, a subquery, a function call).
28447/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
28448/// message stands in for LIMIT / OFFSET, which the caller substitutes.
28449/// All wordings were read off live PG 18.4.
28450fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
28451    use crate::ast::{BinOp, Expr, Literal, UnOp};
28452    match e {
28453        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
28454        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
28455            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
28456        }
28457        // PG coerces a string by its CONTENT, and fails on the value.
28458        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
28459            |_| {
28460                Err(alloc::format!(
28461                    "invalid input syntax for type bigint: \"{t}\""
28462                ))
28463            },
28464            |n| Ok(i128::from(n)),
28465        )),
28466        Expr::Literal(Literal::Bool(_)) => Some(Err(
28467            "argument of {L} must be type bigint, not type boolean".into(),
28468        )),
28469        Expr::Unary {
28470            op: UnOp::Neg,
28471            expr,
28472        } => match fold_limit_constant(expr)? {
28473            Ok(v) => Some(Ok(-v)),
28474            e @ Err(_) => Some(e),
28475        },
28476        Expr::Binary { lhs, op, rhs } => {
28477            let a = match fold_limit_constant(lhs)? {
28478                Ok(v) => v,
28479                e @ Err(_) => return Some(e),
28480            };
28481            let b = match fold_limit_constant(rhs)? {
28482                Ok(v) => v,
28483                e @ Err(_) => return Some(e),
28484            };
28485            let out = match op {
28486                BinOp::Add => a.checked_add(b),
28487                BinOp::Sub => a.checked_sub(b),
28488                BinOp::Mul => a.checked_mul(b),
28489                BinOp::Div if b != 0 => a.checked_div(b),
28490                BinOp::Div => return Some(Err("division by zero".into())),
28491                BinOp::Mod if b != 0 => a.checked_rem(b),
28492                BinOp::Mod => return Some(Err("division by zero".into())),
28493                _ => return None,
28494            };
28495            // PG evaluates the arithmetic in the operand's own type, so an
28496            // int-by-int product that leaves int range fails there — before
28497            // the row count is ever looked at.
28498            match out {
28499                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
28500                    Some(Err("integer out of range".into()))
28501                }
28502                Some(v) => Some(Ok(v)),
28503                None => Some(Err("integer out of range".into())),
28504            }
28505        }
28506        _ => None,
28507    }
28508}
28509
28510/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
28511/// cast, which is what makes `LIMIT 2.5` keep three rows.
28512fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
28513    if scale == 0 {
28514        return unscaled;
28515    }
28516    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
28517        return 0;
28518    };
28519    let neg = unscaled < 0;
28520    let mag = unscaled.unsigned_abs() as i128;
28521    let rounded = (mag + div / 2) / div;
28522    if neg { -rounded } else { rounded }
28523}
28524
28525#[cfg(test)]
28526mod tests {
28527    use super::*;
28528    use alloc::string::ToString;
28529
28530    fn parse(s: &str) -> Statement {
28531        parse_statement(s).expect("parse ok")
28532    }
28533
28534    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
28535    // `tables`, `partition`, etc. are unreserved keywords per PG's
28536    // `pg_get_keywords()` and MUST be usable as column / table /
28537    // alias names. Pre-T4 every drop-in user whose schema had one
28538    // of these as a column name (sentori events.release, mailrs
28539    // messages.index in some forks) blew the parser up at CREATE
28540    // TABLE time with "expected identifier, got Release". The
28541    // generalisation lives in `unreserved_keyword_text` + the
28542    // `expect_ident_like` and `parse_atom` arms that consult it.
28543    #[test]
28544    fn release_usable_as_column_name_in_create_table() {
28545        let stmt =
28546            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
28547        if let Statement::CreateTable(t) = stmt {
28548            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
28549            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
28550        } else {
28551            panic!("expected CreateTable");
28552        }
28553    }
28554
28555    #[test]
28556    fn release_usable_as_column_ref_in_select_projection() {
28557        // The sentori `0003_partition_events.sql` INSERT-SELECT
28558        // walk references `release` in both column lists; the
28559        // projection-side use exercises `parse_atom`'s relaxed
28560        // identifier set.
28561        parse("SELECT id, release, payload FROM events WHERE id = 1");
28562    }
28563
28564    #[test]
28565    fn release_usable_as_column_ref_in_insert_column_list() {
28566        // INSERT INTO t (id, release, payload) VALUES (…)
28567        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
28568    }
28569
28570    #[test]
28571    fn alter_column_drop_not_null_uses_keyword_drop_token() {
28572        // Sentori `0013_audit_tombstone.sql` issues
28573        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
28574        // emits Token::Drop (not Ident("drop")); the parser must
28575        // accept both in the ALTER COLUMN sub-dispatch.
28576        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
28577    }
28578
28579    #[test]
28580    fn create_index_accepts_parenthesised_expression_key() {
28581        // sentori `0040_events_bundle_idx.sql` shape — JSONB
28582        // expression index. Pre-T4 the parser bailed at the
28583        // inner `(` with "expected column ident or expression,
28584        // got LParen". The Token::LParen arm in CREATE INDEX
28585        // routes through the expression parser instead.
28586        parse(
28587            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
28588             ON events ((payload->'bundle'->>'id'))",
28589        );
28590    }
28591
28592    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
28593    // surface as parse errors, never stack overflows (embed hosts
28594    // abort on overflow).
28595    /// The nesting budget is a COUNT; what it has to fit inside is a
28596    /// number of BYTES, and only one of those two is stable across
28597    /// compiler versions. Round 847 measured 30,336 bytes per level
28598    /// after a toolchain move, which puts 64 levels at 1.94 MB and
28599    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
28600    /// aborted instead of erroring, which is precisely the outcome it
28601    /// exists to rule out.
28602    ///
28603    /// So the budget is metered rather than assumed. The ceiling leaves
28604    /// the depth SPG advertises fitting in a default 2 MiB thread with
28605    /// room to spare, in the debug build, where frames are widest.
28606    #[test]
28607    fn nesting_frame_cost_stays_under_ceiling() {
28608        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
28609        // thread keeps a margin for whatever called the parser.
28610        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
28611
28612        frame_meter::reset();
28613        let depth = frame_meter::SAMPLE_HI + 8;
28614        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28615        parse(&sql);
28616
28617        let per_level = frame_meter::bytes_per_level();
28618        {
28619            extern crate std;
28620            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
28621        }
28622        assert!(
28623            per_level <= CEILING,
28624            "{per_level} bytes per nesting level exceeds {CEILING}; \
28625             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
28626             in parse_expr_inner / parse_unary rather than lowering the \
28627             depth or widening the stack.",
28628            per_level * MAX_NEST_DEPTH
28629        );
28630    }
28631
28632    #[test]
28633    fn nesting_budget_errors_cleanly() {
28634        let depth = MAX_NEST_DEPTH + 50;
28635        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28636        let err = parse_statement(&sql).expect_err("must reject");
28637        assert!(err.message.contains("nests deeper"), "{err:?}");
28638        // Within budget still parses.
28639        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
28640        parse(&sql);
28641    }
28642
28643    #[test]
28644    fn binary_chain_budget_errors_cleanly() {
28645        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
28646        let err = parse_statement(&sql).expect_err("must reject");
28647        assert!(err.message.contains("chained binary"), "{err:?}");
28648        // Within budget still parses (chain depth ≤ budget is safe
28649        // for recursive eval/drop on 2 MiB stacks).
28650        let sql = format!("SELECT 1{}", " + 1".repeat(200));
28651        parse(&sql);
28652    }
28653
28654    #[test]
28655    fn in_list_unaffected_by_chain_budget() {
28656        // Flat InList: 20k elements parse fine and stay flat.
28657        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
28658        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
28659        let Statement::Select(s) = parse(&sql) else {
28660            panic!("expected select")
28661        };
28662        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
28663            panic!("expected flat InList, got {:?}", s.where_)
28664        };
28665        assert_eq!(list.len(), 20_000);
28666        assert!(!negated);
28667    }
28668
28669    fn lit_int(n: i64) -> Expr {
28670        Expr::Literal(Literal::Integer(n))
28671    }
28672
28673    fn col(name: &str) -> Expr {
28674        Expr::Column(ColumnName {
28675            qualifier: None,
28676            name: name.into(),
28677        })
28678    }
28679
28680    #[test]
28681    fn select_single_integer() {
28682        let s = parse("SELECT 1");
28683        let Statement::Select(s) = s else {
28684            panic!("expected SELECT")
28685        };
28686        assert_eq!(s.items.len(), 1);
28687        assert!(s.from.is_none());
28688        assert!(s.where_.is_none());
28689    }
28690
28691    #[test]
28692    fn select_multiple_literal_kinds() {
28693        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
28694        let Statement::Select(s) = s else {
28695            panic!("expected SELECT")
28696        };
28697        assert_eq!(s.items.len(), 5);
28698    }
28699
28700    #[test]
28701    fn select_wildcard_from_table() {
28702        let s = parse("SELECT * FROM users");
28703        let Statement::Select(s) = s else {
28704            panic!("expected SELECT")
28705        };
28706        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
28707        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
28708    }
28709
28710    #[test]
28711    fn select_with_table_alias() {
28712        let s = parse("SELECT * FROM users AS u");
28713        let Statement::Select(s) = s else {
28714            panic!("expected SELECT")
28715        };
28716        let t = &s.from.as_ref().unwrap().primary;
28717        assert_eq!(t.name, "users");
28718        assert_eq!(t.alias.as_deref(), Some("u"));
28719    }
28720
28721    #[test]
28722    fn select_with_where_eq() {
28723        let s = parse("SELECT a FROM t WHERE a = 1");
28724        let Statement::Select(s) = s else {
28725            panic!("expected SELECT")
28726        };
28727        let w = s.where_.unwrap();
28728        assert_eq!(
28729            w,
28730            Expr::Binary {
28731                lhs: Box::new(col("a")),
28732                op: BinOp::Eq,
28733                rhs: Box::new(lit_int(1)),
28734            }
28735        );
28736    }
28737
28738    #[test]
28739    fn arithmetic_precedence() {
28740        let s = parse("SELECT 1 + 2 * 3");
28741        let Statement::Select(s) = s else {
28742            panic!("expected SELECT")
28743        };
28744        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28745            panic!("wildcard?")
28746        };
28747        assert_eq!(
28748            expr,
28749            &Expr::Binary {
28750                lhs: Box::new(lit_int(1)),
28751                op: BinOp::Add,
28752                rhs: Box::new(Expr::Binary {
28753                    lhs: Box::new(lit_int(2)),
28754                    op: BinOp::Mul,
28755                    rhs: Box::new(lit_int(3)),
28756                }),
28757            }
28758        );
28759    }
28760
28761    #[test]
28762    fn parentheses_override_precedence() {
28763        let s = parse("SELECT (1 + 2) * 3");
28764        let Statement::Select(s) = s else {
28765            panic!("expected SELECT")
28766        };
28767        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28768            panic!()
28769        };
28770        assert_eq!(
28771            expr,
28772            &Expr::Binary {
28773                lhs: Box::new(Expr::Binary {
28774                    lhs: Box::new(lit_int(1)),
28775                    op: BinOp::Add,
28776                    rhs: Box::new(lit_int(2)),
28777                }),
28778                op: BinOp::Mul,
28779                rhs: Box::new(lit_int(3)),
28780            }
28781        );
28782    }
28783
28784    #[test]
28785    fn not_binds_below_comparison() {
28786        // `NOT a = 1` should parse as `NOT (a = 1)`.
28787        let s = parse("SELECT NOT a = 1 FROM t");
28788        let Statement::Select(s) = s else {
28789            panic!("expected SELECT")
28790        };
28791        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28792            panic!()
28793        };
28794        assert_eq!(
28795            expr,
28796            &Expr::Unary {
28797                op: UnOp::Not,
28798                expr: Box::new(Expr::Binary {
28799                    lhs: Box::new(col("a")),
28800                    op: BinOp::Eq,
28801                    rhs: Box::new(lit_int(1)),
28802                }),
28803            }
28804        );
28805    }
28806
28807    #[test]
28808    fn unary_minus_binds_above_multiplication() {
28809        // `-a * 2` should be `(-a) * 2`.
28810        let s = parse("SELECT -a * 2 FROM t");
28811        let Statement::Select(s) = s else {
28812            panic!("expected SELECT")
28813        };
28814        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28815            panic!()
28816        };
28817        assert_eq!(
28818            expr,
28819            &Expr::Binary {
28820                lhs: Box::new(Expr::Unary {
28821                    op: UnOp::Neg,
28822                    expr: Box::new(col("a")),
28823                }),
28824                op: BinOp::Mul,
28825                rhs: Box::new(lit_int(2)),
28826            }
28827        );
28828    }
28829
28830    #[test]
28831    fn qualified_column() {
28832        let s = parse("SELECT t.col FROM t");
28833        let Statement::Select(s) = s else {
28834            panic!("expected SELECT")
28835        };
28836        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28837            panic!()
28838        };
28839        assert_eq!(
28840            expr,
28841            &Expr::Column(ColumnName {
28842                qualifier: Some("t".into()),
28843                name: "col".into()
28844            })
28845        );
28846    }
28847
28848    #[test]
28849    fn select_item_alias_with_as() {
28850        let s = parse("SELECT a AS y FROM t");
28851        let Statement::Select(s) = s else {
28852            panic!("expected SELECT")
28853        };
28854        let SelectItem::Expr { alias, .. } = &s.items[0] else {
28855            panic!()
28856        };
28857        assert_eq!(alias.as_deref(), Some("y"));
28858    }
28859
28860    #[test]
28861    fn trailing_semicolon_accepted() {
28862        let s = parse("SELECT 1;");
28863        let Statement::Select(s) = s else {
28864            panic!("expected SELECT")
28865        };
28866        assert_eq!(s.items.len(), 1);
28867    }
28868
28869    #[test]
28870    fn boolean_chain_with_and_or_not() {
28871        // (NOT a) OR (b AND (NOT c))
28872        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
28873        let Statement::Select(s) = s else {
28874            panic!("expected SELECT")
28875        };
28876        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28877            panic!()
28878        };
28879        let expected = Expr::Binary {
28880            lhs: Box::new(Expr::Unary {
28881                op: UnOp::Not,
28882                expr: Box::new(col("a")),
28883            }),
28884            op: BinOp::Or,
28885            rhs: Box::new(Expr::Binary {
28886                lhs: Box::new(col("b")),
28887                op: BinOp::And,
28888                rhs: Box::new(Expr::Unary {
28889                    op: UnOp::Not,
28890                    expr: Box::new(col("c")),
28891                }),
28892            }),
28893        };
28894        assert_eq!(expr, &expected);
28895    }
28896
28897    #[test]
28898    fn empty_input_errors() {
28899        // v7.14.0 — pg_dump preambles emit several comment-only
28900        // / blank-line statements that collapse to Statement::
28901        // Empty rather than a parse error. The old "SELECT in
28902        // message" assertion is stale; verify the new contract:
28903        // empty / whitespace / comment-only input parses to
28904        // Statement::Empty.
28905        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
28906        assert!(matches!(
28907            parse_statement("  \n\t ").unwrap(),
28908            Statement::Empty
28909        ));
28910        // Sanity: malformed-but-non-empty still errors.
28911        assert!(parse_statement("SELECT FROM WHERE").is_err());
28912    }
28913
28914    #[test]
28915    fn unmatched_paren_errors() {
28916        assert!(parse_statement("SELECT (1 + 2").is_err());
28917    }
28918
28919    #[test]
28920    fn display_round_trip_simple_select() {
28921        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
28922        let text = original.to_string();
28923        let again = parse_statement(&text).expect("re-parse");
28924        assert_eq!(original, again);
28925    }
28926
28927    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
28928
28929    #[test]
28930    fn create_table_single_column() {
28931        let s = parse("CREATE TABLE foo (a INT)");
28932        let Statement::CreateTable(c) = s else {
28933            panic!("expected CreateTable")
28934        };
28935        assert_eq!(c.name, "foo");
28936        assert_eq!(c.columns.len(), 1);
28937        assert_eq!(c.columns[0].name, "a");
28938        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28939        assert!(c.columns[0].nullable);
28940    }
28941
28942    #[test]
28943    fn create_table_multi_column_with_not_null_mix() {
28944        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
28945        let Statement::CreateTable(c) = s else {
28946            panic!()
28947        };
28948        assert_eq!(c.columns.len(), 4);
28949        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28950        assert!(!c.columns[0].nullable);
28951        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
28952        assert!(c.columns[1].nullable);
28953        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
28954        assert!(!c.columns[2].nullable);
28955        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
28956    }
28957
28958    #[test]
28959    fn create_table_bigint_supported() {
28960        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
28961        let Statement::CreateTable(c) = s else {
28962            panic!()
28963        };
28964        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
28965    }
28966
28967    #[test]
28968    fn create_table_vector_default_is_f32() {
28969        let s = parse("CREATE TABLE t (v VECTOR(128))");
28970        let Statement::CreateTable(c) = s else {
28971            panic!()
28972        };
28973        assert_eq!(
28974            c.columns[0].ty,
28975            ColumnTypeName::Vector {
28976                dim: 128,
28977                encoding: VecEncoding::F32,
28978            },
28979        );
28980    }
28981
28982    #[test]
28983    fn create_table_vector_using_sq8() {
28984        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
28985        // Case-insensitive on both `USING` and the encoding name.
28986        for sql in [
28987            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
28988            "CREATE TABLE t (v VECTOR(128) using sq8)",
28989        ] {
28990            let s = parse(sql);
28991            let Statement::CreateTable(c) = s else {
28992                panic!()
28993            };
28994            assert_eq!(
28995                c.columns[0].ty,
28996                ColumnTypeName::Vector {
28997                    dim: 128,
28998                    encoding: VecEncoding::Sq8,
28999                },
29000                "{sql}",
29001            );
29002        }
29003    }
29004
29005    #[test]
29006    fn create_table_vector_using_unknown_errors() {
29007        // v7.16.1 — the inline `USING <encoding>` shape on
29008        // CREATE TABLE column defs was withdrawn before
29009        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
29010        // (col vector_<metric>_ops)`; the parser now rejects
29011        // USING at column-list position with a clearer
29012        // "expected ',' or ')'" message. Test asserts the
29013        // current rejection, not the old "unknown vector
29014        // encoding" string.
29015        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
29016        assert!(
29017            err.message.contains("USING")
29018                || err.message.contains("using")
29019                || err.message.contains("')'")
29020                || err.message.contains("','"),
29021            "expected USING/column-list rejection, got: {}",
29022            err.message
29023        );
29024    }
29025
29026    #[test]
29027    fn vector_using_sq8_display_roundtrips() {
29028        // The Display impl must produce text that re-parses to the
29029        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
29030        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
29031        let Statement::CreateTable(c) = s else {
29032            panic!()
29033        };
29034        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
29035    }
29036
29037    #[test]
29038    fn parser_recognises_placeholders() {
29039        use crate::ast::{Expr, SelectItem, Statement};
29040        // $N in expression position parses as Expr::Placeholder(N).
29041        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
29042        let Statement::Select(sel) = s else { panic!() };
29043        assert!(matches!(
29044            sel.items[0],
29045            SelectItem::Expr {
29046                expr: Expr::Placeholder(1),
29047                alias: None
29048            }
29049        ));
29050        // $2 + 1
29051        let SelectItem::Expr {
29052            expr: Expr::Binary { lhs, rhs, .. },
29053            ..
29054        } = &sel.items[1]
29055        else {
29056            panic!()
29057        };
29058        assert!(matches!(**lhs, Expr::Placeholder(2)));
29059        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
29060        // WHERE x = $3
29061        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
29062            panic!()
29063        };
29064        assert!(matches!(**rhs, Expr::Placeholder(3)));
29065    }
29066
29067    #[test]
29068    fn parser_rejects_dollar_zero() {
29069        // $0 is not valid in PG; the lexer rejects it.
29070        assert!(parse_statement("SELECT $0").is_err());
29071    }
29072
29073    #[test]
29074    fn placeholder_display_roundtrips() {
29075        // The Display impl must produce text that re-lexes to the
29076        // same Placeholder token.
29077        let s = parse("SELECT $42 FROM t");
29078        let printed = s.to_string();
29079        assert!(printed.contains("$42"));
29080        let again = parse(&printed);
29081        assert_eq!(s, again);
29082    }
29083
29084    #[test]
29085    fn alter_index_rebuild_bare() {
29086        use crate::ast::{AlterIndexTarget, Statement};
29087        let s = parse("ALTER INDEX my_idx REBUILD");
29088        let Statement::AlterIndex(a) = s else {
29089            panic!("expected AlterIndex, got {s:?}")
29090        };
29091        assert_eq!(a.name, "my_idx");
29092        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
29093    }
29094
29095    #[test]
29096    fn alter_index_rebuild_with_encoding() {
29097        use crate::ast::{AlterIndexTarget, Statement};
29098        for (sql, want) in [
29099            (
29100                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
29101                VecEncoding::F32,
29102            ),
29103            (
29104                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
29105                VecEncoding::Sq8,
29106            ),
29107            (
29108                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
29109                VecEncoding::F16,
29110            ),
29111        ] {
29112            let s = parse(sql);
29113            let Statement::AlterIndex(a) = s else {
29114                panic!("{sql}: expected AlterIndex")
29115            };
29116            assert_eq!(a.name, "my_idx");
29117            assert_eq!(
29118                a.target,
29119                AlterIndexTarget::Rebuild {
29120                    encoding: Some(want)
29121                },
29122                "{sql}"
29123            );
29124        }
29125    }
29126
29127    #[test]
29128    fn alter_index_rebuild_unknown_encoding_errors() {
29129        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
29130        assert!(
29131            err.message.contains("unknown vector encoding"),
29132            "got: {}",
29133            err.message
29134        );
29135    }
29136
29137    #[test]
29138    fn alter_index_rebuild_display_roundtrips() {
29139        for (input, want) in [
29140            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
29141            (
29142                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
29143                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
29144            ),
29145            (
29146                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
29147                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
29148            ),
29149        ] {
29150            let s = parse(input);
29151            assert_eq!(s.to_string(), want);
29152        }
29153    }
29154
29155    #[test]
29156    fn create_table_unknown_type_defers_to_engine() {
29157        // v4.9 picked XML as a parse-time "unsupported column
29158        // type" probe. v7.17.0 Phase 1.4 changed the contract:
29159        // an unknown type ident parses as Text + `user_type_ref`
29160        // so CREATE TABLE can resolve user-defined enum / domain
29161        // types — rejection of truly-unknown types moved to the
29162        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
29163        // to a first-class built-in, so this probe switched to a
29164        // synthetic name nothing in the lexer will ever recognise.
29165        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
29166        let Statement::CreateTable(t) = stmt else {
29167            panic!("expected CreateTable");
29168        };
29169        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
29170    }
29171
29172    #[test]
29173    fn create_table_missing_table_keyword_errors() {
29174        assert!(parse_statement("CREATE x (a INT)").is_err());
29175    }
29176
29177    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
29178    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
29179
29180    #[test]
29181    fn parse_create_table_partition_by_range() {
29182        use crate::ast::{PartitionBySpec, PartitionKindAst};
29183        let stmt = parse_statement(
29184            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
29185             payload JSONB) PARTITION BY RANGE (ts)",
29186        )
29187        .unwrap();
29188        let Statement::CreateTable(t) = stmt else {
29189            panic!("expected CreateTable");
29190        };
29191        assert!(t.partition_of.is_none(), "parent has no partition_of");
29192        assert_eq!(t.columns.len(), 3);
29193        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
29194        assert_eq!(
29195            by,
29196            &PartitionBySpec {
29197                kind: PartitionKindAst::Range,
29198                key_columns: alloc::vec!["ts".to_string()],
29199            }
29200        );
29201        // Display round-trip preserves the suffix. `quote_ident`
29202        // only adds double quotes when the ident needs escaping, so
29203        // a plain `ts` survives bare here.
29204        assert!(
29205            t.to_string().contains("PARTITION BY RANGE (ts)"),
29206            "Display lost PARTITION BY suffix: {t}"
29207        );
29208    }
29209
29210    #[test]
29211    fn parse_create_table_partition_of_range() {
29212        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
29213        let stmt = parse_statement(
29214            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
29215             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
29216        )
29217        .unwrap();
29218        let Statement::CreateTable(t) = stmt else {
29219            panic!("expected CreateTable");
29220        };
29221        assert!(t.columns.is_empty(), "child inherits columns from parent");
29222        assert!(t.partition_by.is_none());
29223        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
29224        assert_eq!(of.parent_name, "events_partitioned");
29225        let PartitionOfSpec { bounds, .. } = of.clone();
29226        match bounds {
29227            PartitionOfBoundsAst::Range { lower, upper } => {
29228                assert!(lower.to_string().contains("2026-06-01"));
29229                assert!(upper.to_string().contains("2026-07-01"));
29230            }
29231            other => panic!("expected Range, got {other:?}"),
29232        }
29233        // Display round-trip emits the FOR VALUES tail. `quote_ident`
29234        // skips quotes when not required, so the parent name appears
29235        // bare here.
29236        let s = t.to_string();
29237        assert!(
29238            s.contains("PARTITION OF events_partitioned"),
29239            "Display lost PARTITION OF: {s}"
29240        );
29241        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
29242        assert!(s.contains(") TO ("), "Display lost TO: {s}");
29243    }
29244
29245    #[test]
29246    fn parse_create_table_partition_of_default() {
29247        use crate::ast::PartitionOfBoundsAst;
29248        let stmt =
29249            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
29250                .unwrap();
29251        let Statement::CreateTable(t) = stmt else {
29252            panic!("expected CreateTable");
29253        };
29254        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
29255        assert_eq!(of.parent_name, "events_partitioned");
29256        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
29257        assert!(
29258            t.to_string()
29259                .contains("PARTITION OF events_partitioned DEFAULT"),
29260            "Display lost DEFAULT: {t}"
29261        );
29262    }
29263
29264    #[test]
29265    fn parse_create_table_partition_by_list() {
29266        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
29267        // child with `FOR VALUES IN (lit, lit, …)`.
29268        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
29269        let parent =
29270            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
29271                .unwrap();
29272        let Statement::CreateTable(t) = parent else {
29273            panic!("expected CreateTable");
29274        };
29275        let Some(PartitionBySpec {
29276            kind,
29277            ref key_columns,
29278        }) = t.partition_by
29279        else {
29280            panic!("expected PARTITION BY");
29281        };
29282        assert_eq!(kind, PartitionKindAst::List);
29283        assert_eq!(*key_columns, vec!["region".to_string()]);
29284        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
29285
29286        let child = parse_statement(
29287            "CREATE TABLE events_apac PARTITION OF events_listed \
29288             FOR VALUES IN ('jp', 'kr', 'tw')",
29289        )
29290        .unwrap();
29291        let Statement::CreateTable(c) = child else {
29292            panic!("expected CreateTable");
29293        };
29294        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
29295        let PartitionOfBoundsAst::List { values } = &of.bounds else {
29296            panic!("expected List bounds, got {:?}", of.bounds);
29297        };
29298        assert_eq!(values.len(), 3);
29299        let disp = c.to_string();
29300        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
29301    }
29302
29303    #[test]
29304    fn parse_create_table_partition_by_hash() {
29305        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
29306        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
29307        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
29308        let parent =
29309            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
29310        let Statement::CreateTable(t) = parent else {
29311            panic!("expected CreateTable");
29312        };
29313        let Some(PartitionBySpec {
29314            kind,
29315            ref key_columns,
29316        }) = t.partition_by
29317        else {
29318            panic!("expected PARTITION BY");
29319        };
29320        assert_eq!(kind, PartitionKindAst::Hash);
29321        assert_eq!(*key_columns, vec!["id".to_string()]);
29322        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
29323
29324        let child = parse_statement(
29325            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
29326             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
29327        )
29328        .unwrap();
29329        let Statement::CreateTable(c) = child else {
29330            panic!("expected CreateTable");
29331        };
29332        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
29333        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
29334            panic!("expected Hash bounds");
29335        };
29336        assert_eq!(modulus, 4);
29337        assert_eq!(remainder, 0);
29338        let disp = c.to_string();
29339        assert!(
29340            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
29341            "Display lost HASH bounds: {disp}"
29342        );
29343
29344        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
29345        let bad = parse_statement(
29346            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
29347             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
29348        );
29349        let msg = format!("{}", bad.unwrap_err());
29350        assert!(
29351            msg.contains("REMAINDER") && msg.contains("MODULUS"),
29352            "expected REMAINDER/MODULUS validation error: {msg}"
29353        );
29354    }
29355
29356    #[test]
29357    fn parse_create_table_partition_of_rejects_columns() {
29358        // v7.37.6-B contract: PARTITION OF children inherit columns
29359        // from the parent; an explicit list MUST surface as a parse
29360        // error rather than getting silently ignored.
29361        let err = parse_statement(
29362            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
29363             FOR VALUES FROM ('a') TO ('b')",
29364        );
29365        assert!(err.is_err(), "expected parse error for explicit columns");
29366        let msg = format!("{}", err.unwrap_err());
29367        assert!(
29368            msg.contains("PARTITION OF") && msg.contains("column"),
29369            "error should mention PARTITION OF + columns: {msg}"
29370        );
29371    }
29372
29373    #[test]
29374    fn insert_single_value() {
29375        let s = parse("INSERT INTO foo VALUES (42)");
29376        let Statement::Insert(i) = s else {
29377            panic!("expected Insert")
29378        };
29379        assert_eq!(i.table, "foo");
29380        assert_eq!(i.rows.len(), 1);
29381        assert_eq!(i.rows[0].len(), 1);
29382        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
29383    }
29384
29385    #[test]
29386    fn insert_multi_value_with_mixed_literals() {
29387        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
29388        let Statement::Insert(i) = s else { panic!() };
29389        assert_eq!(i.rows.len(), 1);
29390        assert_eq!(i.rows[0].len(), 5);
29391    }
29392
29393    #[test]
29394    fn insert_missing_into_errors() {
29395        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
29396    }
29397
29398    #[test]
29399    fn create_table_round_trip() {
29400        let original =
29401            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
29402        let text = original.to_string();
29403        let again = parse_statement(&text).expect("re-parse");
29404        assert_eq!(original, again);
29405    }
29406
29407    #[test]
29408    fn insert_round_trip_with_negation_and_string() {
29409        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
29410        let text = original.to_string();
29411        let again = parse_statement(&text).expect("re-parse");
29412        assert_eq!(original, again);
29413    }
29414
29415    #[test]
29416    fn unknown_keyword_at_statement_start_errors() {
29417        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
29418        // the top-level dispatch still has no branch to take.
29419        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
29420        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
29421    }
29422
29423    // --- v0.8 CREATE INDEX --------------------------------------------------
29424
29425    #[test]
29426    fn create_index_basic() {
29427        let s = parse("CREATE INDEX idx_id ON users (id)");
29428        let Statement::CreateIndex(c) = s else {
29429            panic!("expected CreateIndex")
29430        };
29431        assert_eq!(c.name, "idx_id");
29432        assert_eq!(c.table, "users");
29433        assert_eq!(c.column, "id");
29434    }
29435
29436    #[test]
29437    fn create_index_missing_on_errors() {
29438        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
29439    }
29440
29441    #[test]
29442    fn create_index_missing_paren_errors() {
29443        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
29444    }
29445
29446    #[test]
29447    fn create_index_round_trip() {
29448        let original = parse("CREATE INDEX by_name ON users (name)");
29449        let again = parse_statement(&original.to_string()).unwrap();
29450        assert_eq!(original, again);
29451    }
29452
29453    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
29454
29455    #[test]
29456    fn create_unique_index_basic() {
29457        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
29458        let Statement::CreateIndex(c) = s else {
29459            panic!("expected CreateIndex");
29460        };
29461        assert!(c.is_unique);
29462        assert_eq!(c.column, "a");
29463        assert!(c.partial_predicate.is_none());
29464    }
29465
29466    #[test]
29467    fn create_unique_index_partial() {
29468        // mailrs's email_templates "one default per user" shape.
29469        let s = parse(
29470            "CREATE UNIQUE INDEX idx_email_templates_user_default \
29471             ON email_templates (user_address) WHERE is_default = true",
29472        );
29473        let Statement::CreateIndex(c) = s else {
29474            panic!("expected CreateIndex");
29475        };
29476        assert!(c.is_unique);
29477        assert_eq!(c.table, "email_templates");
29478        assert_eq!(c.column, "user_address");
29479        assert!(c.partial_predicate.is_some());
29480    }
29481
29482    #[test]
29483    fn create_unique_index_composite_with_predicate() {
29484        // mailrs's calendar_events instance: composite columns.
29485        let s = parse(
29486            "CREATE UNIQUE INDEX uq_calendar_events_instance \
29487             ON calendar_events (calendar_id, uid, recurrence_id) \
29488             WHERE recurrence_id IS NOT NULL",
29489        );
29490        let Statement::CreateIndex(c) = s else {
29491            panic!("expected CreateIndex");
29492        };
29493        assert!(c.is_unique);
29494        assert_eq!(c.column, "calendar_id");
29495        assert_eq!(
29496            c.extra_columns,
29497            vec!["uid".to_string(), "recurrence_id".to_string()]
29498        );
29499        assert!(c.partial_predicate.is_some());
29500    }
29501
29502    #[test]
29503    fn create_unique_index_using_btree_ok() {
29504        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
29505        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
29506    }
29507
29508    #[test]
29509    fn create_unique_index_using_hnsw_rejected() {
29510        let err =
29511            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
29512        assert!(err.message.contains("UNIQUE"), "{}", err.message);
29513    }
29514
29515    #[test]
29516    fn create_unique_index_round_trip() {
29517        let original = parse(
29518            "CREATE UNIQUE INDEX uq_calendar_events_master \
29519             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
29520        );
29521        let again = parse_statement(&original.to_string()).unwrap();
29522        assert_eq!(original, again);
29523    }
29524
29525    #[test]
29526    fn create_unique_without_index_errors() {
29527        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
29528        // v7.39 (round 340, V56) — PG 18.4, verbatim.
29529        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
29530    }
29531
29532    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
29533
29534    #[test]
29535    fn create_table_bytea_column() {
29536        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
29537        let Statement::CreateTable(c) = s else {
29538            panic!("expected CreateTable");
29539        };
29540        assert_eq!(c.columns.len(), 2);
29541        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
29542        assert!(!c.columns[1].nullable);
29543    }
29544
29545    #[test]
29546    fn create_table_bytes_alias_column() {
29547        let s = parse("CREATE TABLE t (blob BYTES)");
29548        let Statement::CreateTable(c) = s else {
29549            panic!("expected CreateTable");
29550        };
29551        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
29552    }
29553
29554    #[test]
29555    fn bytea_round_trip_display() {
29556        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
29557        let again = parse_statement(&original.to_string()).unwrap();
29558        assert_eq!(original, again);
29559    }
29560
29561    // --- v0.9 transactions -------------------------------------------------
29562
29563    #[test]
29564    fn begin_commit_rollback_parse_as_unit_variants() {
29565        let plain = crate::ast::TransactionModes::default();
29566        assert_eq!(parse("BEGIN"), Statement::Begin(plain));
29567        assert_eq!(parse("COMMIT"), Statement::Commit);
29568        // r1066 — PG synonyms pgbench's tpcb script relies on.
29569        assert_eq!(parse("END"), Statement::Commit);
29570        assert_eq!(parse("END TRANSACTION"), Statement::Commit);
29571        assert_eq!(parse("COMMIT WORK"), Statement::Commit);
29572        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
29573        // Trailing semicolons accepted too.
29574        assert_eq!(parse("BEGIN;"), Statement::Begin(plain));
29575        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
29576        // statement (with or without the WORK/TRANSACTION noise word).
29577        assert_eq!(
29578            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
29579            Statement::Begin(crate::ast::TransactionModes {
29580                isolation: Some(IsolationLevel::RepeatableRead),
29581                read_only: None,
29582                deferrable: None,
29583            })
29584        );
29585        assert_eq!(
29586            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
29587            Statement::Begin(crate::ast::TransactionModes {
29588                isolation: Some(IsolationLevel::Serializable),
29589                read_only: None,
29590                deferrable: None,
29591            })
29592        );
29593        // v7.39 — this line used to read
29594        //
29595        //     // A non-isolation mode keeps the session default (None).
29596        //     assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
29597        //
29598        // which pinned the defect rather than catching it: the READ ONLY
29599        // was thrown away, so the statement opened an ordinary read-write
29600        // transaction and every write inside it was accepted. The
29601        // isolation level is still absent here, because this statement
29602        // does not name one — that part was right.
29603        assert_eq!(
29604            parse("BEGIN READ ONLY"),
29605            Statement::Begin(crate::ast::TransactionModes {
29606                isolation: None,
29607                read_only: Some(true),
29608                deferrable: None,
29609            })
29610        );
29611        assert_eq!(
29612            parse("START TRANSACTION READ WRITE"),
29613            Statement::Begin(crate::ast::TransactionModes {
29614                isolation: None,
29615                read_only: Some(false),
29616                deferrable: None,
29617            })
29618        );
29619        assert_eq!(
29620            parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY"),
29621            Statement::Begin(crate::ast::TransactionModes {
29622                isolation: Some(IsolationLevel::Serializable),
29623                read_only: Some(true),
29624                deferrable: None,
29625            })
29626        );
29627        // v7.40.12 — DEFERRABLE was consumed and dropped, the way READ
29628        // ONLY was before v7.39, and for the same reason nothing noticed:
29629        // the statement parsed, so the clause looked handled. Measured on
29630        // PG 18.6, `BEGIN DEFERRABLE; SHOW transaction_deferrable` -> on.
29631        assert_eq!(
29632            parse("BEGIN DEFERRABLE"),
29633            Statement::Begin(crate::ast::TransactionModes {
29634                isolation: None,
29635                read_only: None,
29636                deferrable: Some(true),
29637            })
29638        );
29639        assert_eq!(
29640            parse("BEGIN NOT DEFERRABLE"),
29641            Statement::Begin(crate::ast::TransactionModes {
29642                isolation: None,
29643                read_only: None,
29644                deferrable: Some(false),
29645            })
29646        );
29647        assert_eq!(
29648            parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY, DEFERRABLE"),
29649            Statement::Begin(crate::ast::TransactionModes {
29650                isolation: Some(IsolationLevel::Serializable),
29651                read_only: Some(true),
29652                deferrable: Some(true),
29653            })
29654        );
29655    }
29656
29657    /// v7.40.12 — PG's multi-word SHOW spellings. `SHOW TIME ZONE` was
29658    /// known only to a pgwire shortcut that answered SHOW from a copy;
29659    /// when that shortcut went, the spelling had to live here, where the
29660    /// embedded API gets it too. `SHOW SESSION AUTHORIZATION` was known
29661    /// nowhere: `session` is a bare ident, so the statement ended there
29662    /// and the next word was a syntax error.
29663    #[test]
29664    fn show_multi_word_spellings_parse() {
29665        assert_eq!(
29666            parse("SHOW TIME ZONE"),
29667            Statement::ShowParameter("timezone".to_string())
29668        );
29669        assert_eq!(
29670            parse("SHOW SESSION AUTHORIZATION"),
29671            Statement::ShowParameter("session_authorization".to_string())
29672        );
29673        assert_eq!(
29674            parse("SHOW TRANSACTION ISOLATION LEVEL"),
29675            Statement::ShowParameter("transaction_isolation".to_string())
29676        );
29677        // The single-word spellings still reach the same handler.
29678        assert_eq!(
29679            parse("SHOW timezone"),
29680            Statement::ShowParameter("timezone".to_string())
29681        );
29682        assert_eq!(
29683            parse("SHOW session_authorization"),
29684            Statement::ShowParameter("session_authorization".to_string())
29685        );
29686    }
29687
29688    // --- v1.2: pgvector distance ops + ::vector cast --------------------
29689
29690    #[test]
29691    fn inner_product_binop_parses() {
29692        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
29693        let Statement::Select(s) = s else { panic!() };
29694        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29695            panic!()
29696        };
29697        assert!(matches!(
29698            expr,
29699            Expr::Binary {
29700                op: BinOp::InnerProduct,
29701                ..
29702            }
29703        ));
29704    }
29705
29706    #[test]
29707    fn cosine_distance_binop_parses() {
29708        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
29709        let Statement::Select(s) = s else { panic!() };
29710        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29711            panic!()
29712        };
29713        assert!(matches!(
29714            expr,
29715            Expr::Binary {
29716                op: BinOp::CosineDistance,
29717                ..
29718            }
29719        ));
29720    }
29721
29722    #[test]
29723    fn vector_cast_postfix_wraps_string_literal() {
29724        let s = parse("SELECT '[1,2,3]'::vector FROM t");
29725        let Statement::Select(s) = s else { panic!() };
29726        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29727            panic!()
29728        };
29729        assert!(matches!(
29730            expr,
29731            Expr::Cast {
29732                target: CastTarget::Vector,
29733                ..
29734            }
29735        ));
29736    }
29737
29738    #[test]
29739    fn unsupported_cast_target_errors() {
29740        // v7.37.5 ship triage promoted the parser to accept every
29741        // ident as a `CastTarget::Named(canonical)`; the engine
29742        // surfaces the "unsupported cast target" error at eval
29743        // time when `type_name_to_data_type` can't resolve it.
29744        // Parser-side error now requires a NON-ident after `::`
29745        // (e.g. a punctuation token).
29746        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
29747        assert_eq!(err.message, "syntax error at or near \",\"");
29748    }
29749
29750    #[test]
29751    fn tx_statements_round_trip() {
29752        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
29753            let original = parse(q);
29754            let again = parse_statement(&original.to_string()).unwrap();
29755            assert_eq!(original, again);
29756        }
29757    }
29758
29759    #[test]
29760    fn interval_text_parsing_units() {
29761        // v7.37.5 β — three-field shape `(months, days, micros)` so
29762        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
29763        // Single unit.
29764        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
29765        assert_eq!(
29766            parse_interval_text("24 hours"),
29767            Some((0, 0, 86_400_000_000))
29768        );
29769        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
29770        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
29771        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
29772        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
29773        // Compound spans accumulate per-dimension.
29774        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
29775        assert_eq!(
29776            parse_interval_text("1 day 2 hours"),
29777            Some((0, 1, 7_200_000_000))
29778        );
29779        // Negative numbers carry through per-dimension.
29780        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
29781        // Bad shapes return None.
29782        assert_eq!(parse_interval_text(""), None);
29783        assert_eq!(parse_interval_text("garbage"), None);
29784        assert_eq!(parse_interval_text("1 fortnight"), None);
29785        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
29786        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
29787        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
29788        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
29789        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
29790    }
29791
29792    #[test]
29793    fn interval_literal_roundtrips_via_display() {
29794        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
29795        let s = parsed.to_string();
29796        // Display preserves the original text verbatim.
29797        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
29798        // And re-parsing yields a structurally equal statement.
29799        let again = parse_statement(&s).unwrap();
29800        assert_eq!(parsed, again);
29801    }
29802
29803    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
29804
29805    #[test]
29806    fn parser_recognises_create_publication_bare() {
29807        let s = parse("CREATE PUBLICATION pub_a");
29808        let Statement::CreatePublication(p) = s else {
29809            panic!("expected CreatePublication, got {s:?}")
29810        };
29811        assert_eq!(p.name, "pub_a");
29812        assert_eq!(p.scope, PublicationScope::AllTables);
29813    }
29814
29815    #[test]
29816    fn parser_recognises_create_publication_for_all_tables() {
29817        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
29818        let Statement::CreatePublication(p) = s else {
29819            panic!("expected CreatePublication, got {s:?}")
29820        };
29821        assert_eq!(p.name, "pub_a");
29822        assert_eq!(p.scope, PublicationScope::AllTables);
29823    }
29824
29825    #[test]
29826    fn parser_recognises_drop_publication() {
29827        let s = parse("DROP PUBLICATION pub_a");
29828        let Statement::DropPublication { name, .. } = s else {
29829            panic!("expected DropPublication, got {s:?}")
29830        };
29831        assert_eq!(name, "pub_a");
29832    }
29833
29834    #[test]
29835    fn parser_recognises_for_table_list() {
29836        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
29837        let Statement::CreatePublication(p) = s else {
29838            panic!("expected CreatePublication, got {s:?}")
29839        };
29840        assert_eq!(p.name, "pub_a");
29841        let PublicationScope::ForTables(ts) = p.scope else {
29842            panic!("expected ForTables scope")
29843        };
29844        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
29845    }
29846
29847    #[test]
29848    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
29849        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
29850        // is rejected (`invalid publication object list`; the old
29851        // test pinned an unverifiable "PG 19 accepts both" claim);
29852        // TABLES pairs with IN SCHEMA.
29853        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
29854            .expect_err("bare FOR TABLES must reject");
29855        assert!(
29856            alloc::format!("{err}").contains("invalid publication object list"),
29857            "got: {err}"
29858        );
29859        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
29860        let Statement::CreatePublication(p) = s else {
29861            panic!("expected CreatePublication, got {s:?}")
29862        };
29863        let PublicationScope::TablesInSchema(schema) = p.scope else {
29864            panic!("expected TablesInSchema")
29865        };
29866        assert_eq!(schema, "public");
29867    }
29868
29869    #[test]
29870    fn parser_recognises_for_all_tables_except_list() {
29871        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
29872        let Statement::CreatePublication(p) = s else {
29873            panic!()
29874        };
29875        let PublicationScope::AllTablesExcept(ts) = p.scope else {
29876            panic!("expected AllTablesExcept")
29877        };
29878        assert_eq!(ts, alloc::vec!["t1", "t2"]);
29879    }
29880
29881    #[test]
29882    fn parser_rejects_for_table_with_empty_list() {
29883        // `FOR TABLE` with nothing after is a parse error.
29884        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
29885            .expect_err("must error on empty list");
29886        // No specific message asserted — the call falls through to
29887        // expect_ident_like which yields "expected identifier, got …".
29888        assert!(!err.message.is_empty());
29889    }
29890
29891    #[test]
29892    fn parser_recognises_show_publications() {
29893        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
29894        // bare ident in this position, NOT a reserved keyword.
29895        let s = parse("SHOW PUBLICATIONS");
29896        assert!(matches!(s, Statement::ShowPublications));
29897    }
29898
29899    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
29900
29901    #[test]
29902    fn parser_recognises_create_subscription_single_publication() {
29903        let s = parse(
29904            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
29905        );
29906        let Statement::CreateSubscription(c) = s else {
29907            panic!("expected CreateSubscription, got {s:?}")
29908        };
29909        assert_eq!(c.name, "sub_a");
29910        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
29911        assert_eq!(c.publications, alloc::vec!["pub_a"]);
29912    }
29913
29914    #[test]
29915    fn parser_recognises_create_subscription_multi_publication() {
29916        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
29917        let Statement::CreateSubscription(c) = s else {
29918            panic!()
29919        };
29920        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
29921    }
29922
29923    #[test]
29924    fn parser_rejects_create_subscription_missing_connection() {
29925        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
29926            .expect_err("must error on missing CONNECTION");
29927        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
29928    }
29929
29930    #[test]
29931    fn parser_rejects_create_subscription_missing_publication() {
29932        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
29933            .expect_err("must error on missing PUBLICATION");
29934        assert_eq!(err.message, "syntax error at end of input");
29935    }
29936
29937    #[test]
29938    fn parser_recognises_drop_subscription() {
29939        let s = parse("DROP SUBSCRIPTION sub_a");
29940        let Statement::DropSubscription { name, .. } = s else {
29941            panic!("expected DropSubscription, got {s:?}")
29942        };
29943        assert_eq!(name, "sub_a");
29944    }
29945
29946    #[test]
29947    fn parser_recognises_show_subscriptions() {
29948        let s = parse("SHOW SUBSCRIPTIONS");
29949        assert!(matches!(s, Statement::ShowSubscriptions));
29950    }
29951
29952    #[test]
29953    fn parser_recognises_wait_for_wal_position_no_timeout() {
29954        let s = parse("WAIT FOR WAL POSITION 12345");
29955        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29956            panic!("expected WaitForWalPosition, got {s:?}")
29957        };
29958        assert_eq!(pos, 12345);
29959        assert!(timeout_ms.is_none());
29960    }
29961
29962    #[test]
29963    fn parser_recognises_wait_for_wal_position_with_timeout() {
29964        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
29965        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29966            panic!()
29967        };
29968        assert_eq!(pos, 67890);
29969        assert_eq!(timeout_ms, Some(5000));
29970    }
29971
29972    #[test]
29973    fn parser_rejects_wait_with_negative_position() {
29974        // The lexer treats `-` as a token; `expect_u64_literal`
29975        // only sees the Integer that follows, so the negative
29976        // arrives as a unary-minus expression at higher levels.
29977        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
29978        // parse error one way or another.
29979        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
29980        assert!(!err.message.is_empty());
29981    }
29982
29983    #[test]
29984    fn parser_recognises_bare_analyze() {
29985        let s = parse("ANALYZE");
29986        assert!(matches!(s, Statement::Analyze(None)));
29987    }
29988
29989    #[test]
29990    fn parser_recognises_analyze_with_table() {
29991        let s = parse("ANALYZE users");
29992        let Statement::Analyze(Some(name)) = s else {
29993            panic!("expected Analyze, got {s:?}")
29994        };
29995        assert_eq!(name, "users");
29996    }
29997
29998    #[test]
29999    fn parser_recognises_analyze_with_quoted_table() {
30000        let s = parse("ANALYZE \"Mixed Case\"");
30001        let Statement::Analyze(Some(name)) = s else {
30002            panic!()
30003        };
30004        assert_eq!(name, "Mixed Case");
30005    }
30006
30007    #[test]
30008    fn parser_rejects_analyze_with_garbage_token() {
30009        let err = parse_statement("ANALYZE 42").expect_err("must error");
30010        assert!(!err.message.is_empty());
30011    }
30012
30013    #[test]
30014    fn analyze_display_roundtrips() {
30015        for sql in ["ANALYZE", "ANALYZE users"] {
30016            let s = parse(sql);
30017            let printed = s.to_string();
30018            let again = parse_statement(&printed)
30019                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
30020            assert_eq!(s, again);
30021        }
30022    }
30023
30024    #[test]
30025    fn wait_for_display_roundtrips() {
30026        for sql in [
30027            "WAIT FOR WAL POSITION 12345",
30028            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
30029        ] {
30030            let s = parse(sql);
30031            let printed = s.to_string();
30032            let again = parse_statement(&printed)
30033                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
30034            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
30035        }
30036    }
30037
30038    #[test]
30039    fn subscription_ddl_display_roundtrips() {
30040        for sql in [
30041            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
30042            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
30043            "DROP SUBSCRIPTION sub_a",
30044            "SHOW SUBSCRIPTIONS",
30045        ] {
30046            let s = parse(sql);
30047            let printed = s.to_string();
30048            let again = parse_statement(&printed)
30049                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
30050            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
30051        }
30052    }
30053
30054    #[test]
30055    fn parser_drop_dispatches_user_vs_publication() {
30056        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
30057        // tokenises DROP. Both targets must still parse.
30058        let s = parse("DROP USER 'alice'");
30059        let Statement::DropUser { name, .. } = s else {
30060            panic!("expected DropUser, got {s:?}")
30061        };
30062        assert_eq!(name, "alice");
30063        // And DROP PUBLICATION lands the new variant.
30064        let s = parse("DROP PUBLICATION p1");
30065        assert!(matches!(s, Statement::DropPublication { .. }));
30066    }
30067
30068    #[test]
30069    fn publication_ddl_display_roundtrips() {
30070        // Every CREATE PUBLICATION variant must Display → parse →
30071        // same AST. v6.1.3 covers all three scope shapes.
30072        for sql in [
30073            "CREATE PUBLICATION pub_a",
30074            "CREATE PUBLICATION pub_a FOR ALL TABLES",
30075            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
30076            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
30077            "DROP PUBLICATION pub_a",
30078            "SHOW PUBLICATIONS",
30079        ] {
30080            let s = parse(sql);
30081            let printed = s.to_string();
30082            let again = parse_statement(&printed)
30083                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
30084            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
30085        }
30086    }
30087
30088    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
30089
30090    #[test]
30091    fn create_function_returns_trigger_plpgsql_minimal() {
30092        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
30093        let s = parse(sql);
30094        let Statement::CreateFunction(f) = s else {
30095            panic!("expected CreateFunction");
30096        };
30097        assert_eq!(f.name, "noop");
30098        assert!(!f.or_replace);
30099        assert!(f.args.is_empty());
30100        assert!(matches!(f.returns, FunctionReturn::Trigger));
30101        assert_eq!(f.language, "plpgsql");
30102        let FunctionBody::PlPgSql(block) = f.body else {
30103            panic!("expected PlPgSql body");
30104        };
30105        assert_eq!(block.statements.len(), 1);
30106        assert!(matches!(
30107            block.statements[0],
30108            PlPgSqlStmt::Return(ReturnTarget::New)
30109        ));
30110    }
30111
30112    #[test]
30113    fn create_function_or_replace_with_assignment() {
30114        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
30115        // RETURN NEW.
30116        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
30117BEGIN
30118  NEW.search_vector := to_tsvector('english', NEW.subject);
30119  RETURN NEW;
30120END;
30121$$";
30122        let s = parse(sql);
30123        let Statement::CreateFunction(f) = s else {
30124            panic!("expected CreateFunction");
30125        };
30126        assert!(f.or_replace);
30127        let FunctionBody::PlPgSql(block) = &f.body else {
30128            panic!("expected PlPgSql body");
30129        };
30130        assert_eq!(block.statements.len(), 2);
30131        // First statement: NEW.search_vector := to_tsvector(...)
30132        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
30133            panic!("expected Assign as first stmt");
30134        };
30135        match target {
30136            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
30137            other => panic!("expected NEW.col, got {other:?}"),
30138        }
30139        // Second statement: RETURN NEW
30140        assert!(matches!(
30141            block.statements[1],
30142            PlPgSqlStmt::Return(ReturnTarget::New)
30143        ));
30144    }
30145
30146    #[test]
30147    fn create_trigger_after_insert_or_update() {
30148        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
30149        let s = parse(sql);
30150        let Statement::CreateTrigger(t) = s else {
30151            panic!("expected CreateTrigger");
30152        };
30153        assert_eq!(t.name, "tg");
30154        assert_eq!(t.table, "messages");
30155        assert_eq!(t.timing, TriggerTiming::After);
30156        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
30157        assert_eq!(t.for_each, TriggerForEach::Row);
30158        assert_eq!(t.function, "update_sv");
30159    }
30160
30161    #[test]
30162    fn create_trigger_before_delete_execute_procedure_alias() {
30163        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
30164        let sql =
30165            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
30166        let s = parse(sql);
30167        let Statement::CreateTrigger(t) = s else {
30168            panic!("expected CreateTrigger");
30169        };
30170        assert_eq!(t.timing, TriggerTiming::Before);
30171        assert_eq!(t.events, vec![TriggerEvent::Delete]);
30172    }
30173
30174    #[test]
30175    fn drop_trigger_if_exists_round_trips() {
30176        // No parser support for DROP TRIGGER yet — added in v7.12.5
30177        // alongside the broader DROP …{IF EXISTS} cleanup. The
30178        // AST + Display impls are in place so we round-trip via
30179        // construction:
30180        let s = Statement::DropTrigger {
30181            name: "tg".into(),
30182            table: "messages".into(),
30183            if_exists: true,
30184        };
30185        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
30186    }
30187
30188    #[test]
30189    fn trigger_ddl_display_roundtrips_through_parser() {
30190        // CREATE TRIGGER + its referenced CREATE FUNCTION must
30191        // Display → parse → same AST (modulo PL/pgSQL body
30192        // formatting which is parser-canonicalised).
30193        for sql in [
30194            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
30195            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
30196        ] {
30197            let s = parse(sql);
30198            let printed = s.to_string();
30199            let again = parse_statement(&printed)
30200                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
30201            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
30202        }
30203    }
30204}