Skip to main content

spg_sql/
parser.rs

1//! Recursive-descent parser with a Pratt (precedence-climbing) sub-parser for
2//! expressions.
3//!
4//! Precedence (lowest → highest binding):
5//! `OR` (1) `<` `AND` (2) `<` `NOT` unary (3) `<`
6//! comparisons `=` `<>` `<` `<=` `>` `>=` (4) `<`
7//! `+` `-` (5) `<` `*` `/` (6) `<` unary `-` (7) `<` parens / atom.
8//!
9//! This matches PG's behaviour for the operators we support — e.g. `NOT a = b`
10//! parses as `NOT (a = b)` and `-a * b` as `(-a) * b`.
11
12use alloc::boxed::Box;
13use alloc::format;
14use alloc::string::{String, ToString};
15use alloc::vec;
16use alloc::vec::Vec;
17use core::fmt;
18use core::mem;
19
20use crate::ast::{
21    AssignTarget, BinOp, CastTarget, Collation, ColumnDef, ColumnName, ColumnTypeName,
22    CreateFunctionStatement, CreateIndexStatement, CreatePublicationStatement,
23    CreateSubscriptionStatement, CreateTableStatement, CreateTriggerStatement, DiscardTarget, Expr,
24    ExtractField, FkAction, ForeignKeyConstraint, FrameBound, FrameExclusion, FrameKind,
25    FromClause, FromJoin, FunctionArg, FunctionArgMode, FunctionArgType, FunctionAttrs,
26    FunctionBody, FunctionParallel, FunctionReturn, FunctionVolatility, GrantObject, GrantPriv,
27    GrantStatement, IndexMethod, InsertStatement, IsolationLevel, JoinKind, Literal, MysqlIntWidth,
28    NullTreatment, OrderBy, Overriding, PlPgSqlBlock, PlPgSqlDeclare, PlPgSqlStmt,
29    PublicationScope, RaiseLevel, RangeKindAst, ReturnTarget, SelectItem, SelectStatement,
30    Statement, TableRef, TriggerEvent, TriggerForEach, TriggerTiming, UnOp, UnionKind, VecEncoding,
31    WindowFrame,
32};
33use crate::lexer::{self, LexError, Token};
34
35/// v7.38 — a `WINDOW w AS (…)` definition body:
36/// `(PARTITION BY exprs, ORDER BY (expr, desc, nulls_first), frame)`.
37type WindowDef = (
38    Vec<Expr>,
39    Vec<(Expr, bool, Option<bool>)>,
40    Option<WindowFrame>,
41);
42
43/// v7.14.0 — true when the leading keyword of a top-level
44/// statement is one of the dump-emitted DDL forms SPG accepts
45/// as a no-op (no behavioural effect on the single-schema /
46/// single-database model). These statements are consumed up to
47/// the next `;` / EOF and returned as `Statement::Empty`.
48/// v7.39 (read01 round 57) — wrap a parsed GRANT body in the right statement.
49fn finish_grant(grant: bool, g: GrantStatement) -> Statement {
50    if grant {
51        Statement::Grant(g)
52    } else {
53        Statement::Revoke(g)
54    }
55}
56
57fn is_dump_noise_statement(lc: &str) -> bool {
58    matches!(
59        lc,
60        // v7.39 (read01 round 50): "comment" moved OUT — COMMENT ON is now a
61        // real statement with a real store. v7.39 (read01 round 57): "grant" /
62        // "revoke" moved OUT — table privileges are now REAL (stored in
63        // `relacl`, enforced against the session role); a grant on any other
64        // object class still parses and no-ops so dumps restore.
65        // MySQL bulk-load brackets.
66        "unlock"
67            // MySQL OPTIMIZE / ANALYZE TABLE / CHECK TABLE
68            // diagnostics that pg_dump-style tools also emit
69            // post-restore.
70            | "optimize"
71            | "check"
72            // PG psql backslash meta-commands that newer
73            // pg_dump versions emit unescaped (\restrict /
74            // \unrestrict). Real psql intercepts these; SPG's
75            // PG-wire sees them as raw text.
76            | "\\restrict"
77            | "\\unrestrict"
78            // v7.17.0 Phase 4.1 — MySQL `DELIMITER //` and
79            // `DELIMITER ;` directives. Technically client-side
80            // (the `mysql` CLI uses them to set the statement
81            // terminator), not SQL — but mysqldump and stored-
82            // procedure scripts emit them inline. SPG's parser
83            // sees one statement at a time and doesn't care
84            // about the terminator, so consume DELIMITER lines
85            // as Empty.
86            | "delimiter"
87            // v7.37.17 (17.6 siblings) — additional PG maintenance /
88            // session-state statements pg_dump + application startup
89            // scripts emit. SPG has no matching session-state to
90            // discard (no prepared-plan cache surface, no temp
91            // sequences), no matching security-label / storage-
92            // option to apply, no separate CREATE/DROP CAST that
93            // affects execution.
94            // v7.37.17 (17.6 siblings) — PG role-cleanup statements
95            // pg_dump / pg_dumpall emit around DROP ROLE:
96            //   REASSIGN OWNED BY <role> [, ...] TO <newrole>
97            //   DROP OWNED BY <role> [, ...] [CASCADE | RESTRICT]
98            // Both operate on the role's owned objects; SPG has no
99            // role-owner model, so accept-and-no-op.
100            // v7.37.17 (17.6 sibling) — LOAD '<library>'. pg_dump
101            // + extension scripts use LOAD to preload shared
102            // libraries. SPG doesn't have a shared-library extension
103            // point today (extensions ship as first-class crates
104            // linked at build time); accept as a no-op.
105            | "load"
106    )
107}
108
109/// v7.37.43-T4 — PG-unreserved keywords that are legal identifiers
110/// per `pg_get_keywords()`. SPG tokenizes these as named variants
111/// so the parser can dispatch on them in their owning contexts
112/// (`RELEASE SAVEPOINT`, `SHOW name`, `BEGIN`/`COMMIT`/`ROLLBACK`,
113/// `CREATE INDEX`, etc.), but they MUST stay usable as table /
114/// column / alias names — that's the PG contract for unreserved
115/// keywords (see PG docs Appendix C.1).
116///
117/// Before this generalisation, sentori migration 0001_init.sql
118/// `release TEXT NOT NULL` blew up the parser with "expected
119/// identifier, got Release", and the same gap stalked every
120/// SPG drop-in user whose schema had a column / alias named
121/// `release` / `index` / `tables` / `show` / `savepoint` /
122/// `begin` / `commit` / `rollback` / `drop` / `insert` / `values`
123/// / `limit` / `partition`. PG accepts all of them as identifiers
124/// when unquoted, so SPG must too.
125///
126/// Returns the canonical lowercase identifier text when the token
127/// belongs to PG's unreserved class, `None` otherwise. Used by
128/// `expect_ident_like` (column / table / alias names) so the
129/// generalisation applies everywhere an identifier may appear,
130/// not just in the contexts these tokens were introduced for.
131fn unreserved_keyword_text(tok: &Token) -> Option<String> {
132    let s = match tok {
133        // PG keyword class: unreserved or col_name.
134        //
135        Token::Release => "release",
136        Token::Savepoint => "savepoint",
137        Token::Show => "show",
138        Token::Index => "index",
139        Token::Begin => "begin",
140        Token::Commit => "commit",
141        Token::Rollback => "rollback",
142        Token::Drop => "drop",
143        Token::Insert => "insert",
144        Token::Values => "values",
145        Token::Limit => "limit",
146        Token::Partition => "partition",
147        Token::Tables => "tables",
148        Token::Connection => "connection",
149        Token::Publication => "publication",
150        Token::Subscription => "subscription",
151        Token::Interval => "interval",
152        // `extract` is non-reserved in PG too (it's a function the
153        // parser dispatches via context — outside that context it's
154        // a plain identifier).
155        Token::Extract => "extract",
156        Token::Offset => "offset",
157        // `to` is reserved in PG (used in many "AS … TO …" forms), so
158        // it is NOT relaxed here. Same for `from`, `where`, `as`,
159        // `select`, `not`, `and`, `or`, `null`, `true`, `false`,
160        // `create`, `table`, `into`, `on`, `order`, `by`, `having`,
161        // `group`, `distinct`, `union`, `all`, `join`, `inner`,
162        // `left`, `cross`, `outer`, `default`, `is`, `between`,
163        // `in`, `like`, `for`, `except`, `desc`, `asc`, `partition`
164        // (partial — keep partition as unreserved per modern PG).
165        _ => return None,
166    };
167    Some(s.to_string())
168}
169
170/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
171/// in CREATE INDEX. SPG's HNSW already routes by query operator;
172/// the opclass is accepted for `pg_dump` compatibility (mailrs
173/// migration follow-up G5).
174/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
175/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
176/// doesn't change index behaviour based on them.
177/// v7.37.17 (17.6 siblings) — the four PG `each` SRFs share one
178/// FROM-clause pipeline; the stored name tells the executor whether
179/// the value column keeps JSON rendering (`jsonb_each` / `json_each`)
180/// or unwraps to text (`*_each_text`).
181fn is_json_each_name(s: &str) -> bool {
182    s.eq_ignore_ascii_case("jsonb_each_text")
183        || s.eq_ignore_ascii_case("jsonb_each")
184        || s.eq_ignore_ascii_case("json_each_text")
185        || s.eq_ignore_ascii_case("json_each")
186}
187
188/// v7.38 (read01, T14) — resolve named function arguments (`argname => value`)
189/// to positional order for the `make_*` family (the AST stays positional).
190/// Positional args fill slots left-to-right; a named arg goes to its registered
191/// slot; unfilled slots default to integer 0 (PG's optional make_interval
192/// fields — the make_date/time arity is still checked at eval time).
193fn reorder_named_args(
194    fname: &str,
195    args: Vec<Expr>,
196    names: &[Option<String>],
197) -> Result<Vec<Expr>, String> {
198    let params: &[&str] = match fname.to_ascii_lowercase().as_str() {
199        "make_date" => &["year", "month", "day"],
200        "make_time" => &["hour", "min", "sec"],
201        "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
202        "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
203        other => {
204            return Err(alloc::format!(
205                "function {other}(...) does not support named arguments"
206            ));
207        }
208    };
209    let mut slots: Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
210    let mut next_positional = 0usize;
211    for (arg, name) in args.into_iter().zip(names.iter()) {
212        let idx = match name {
213            Some(n) => params
214                .iter()
215                .position(|p| p.eq_ignore_ascii_case(n))
216                .ok_or_else(|| alloc::format!("{fname}(...) has no argument named \"{n}\""))?,
217            None => {
218                let i = next_positional;
219                next_positional += 1;
220                i
221            }
222        };
223        if idx >= slots.len() {
224            return Err(alloc::format!("too many arguments for {fname}(...)"));
225        }
226        if slots[idx].is_some() {
227            return Err(alloc::format!(
228                "argument \"{}\" specified more than once",
229                params[idx]
230            ));
231        }
232        slots[idx] = Some(arg);
233    }
234    Ok(slots
235        .into_iter()
236        .map(|s| s.unwrap_or(Expr::Literal(Literal::Integer(0))))
237        .collect())
238}
239
240/// v7.38 (read01) — parse a lexer `Token::Numeric` source string (digits with
241/// an optional single `.`, no sign, no exponent) into `(unscaled, scale)` for
242/// `Literal::Numeric`. Returns `None` if the mantissa overflows i128.
243/// v7.39 (read01 numeric.c) — the result of expanding an `1.5e3`-style
244/// scientific literal into PG's plain NUMERIC decimal form.
245#[derive(Debug)]
246pub enum SciExpanded {
247    /// Plain decimal string ("1.5e3" → "1500", "1e-5" → "0.00001").
248    Expanded(String),
249    /// Exponent pushes the value outside PG's numeric format
250    /// (more than 131072 integer digits or 16383 fractional digits).
251    Overflow,
252    /// Not a `[±]digits[.digits]e[±]digits` literal at all.
253    NotScientific,
254}
255
256/// Expand scientific notation into a plain decimal string by moving the
257/// decimal point — no float round-trip, so the value stays exact. PG treats
258/// such literals as NUMERIC; the digit-count caps mirror PG's numeric format
259/// limits ("value overflows numeric format").
260pub fn expand_scientific_literal(s: &str) -> SciExpanded {
261    let s = s.trim();
262    let Some(epos) = s.find(['e', 'E']) else {
263        return SciExpanded::NotScientific;
264    };
265    let (mant, exp_str) = (&s[..epos], &s[epos + 1..]);
266    let Ok(exp) = exp_str.parse::<i64>() else {
267        return SciExpanded::NotScientific;
268    };
269    let (neg, mant) = match mant.strip_prefix('-') {
270        Some(r) => (true, r),
271        None => (false, mant.strip_prefix('+').unwrap_or(mant)),
272    };
273    let (int_part, frac_part) = match mant.split_once('.') {
274        Some((i, f)) => (i, f),
275        None => (mant, ""),
276    };
277    if (int_part.is_empty() && frac_part.is_empty())
278        || !int_part.bytes().all(|b| b.is_ascii_digit())
279        || !frac_part.bytes().all(|b| b.is_ascii_digit())
280    {
281        return SciExpanded::NotScientific;
282    }
283    let mut digits = String::with_capacity(int_part.len() + frac_part.len());
284    digits.push_str(int_part);
285    digits.push_str(frac_part);
286    // Decimal point position within `digits` after applying the exponent.
287    let new_point = int_part.len() as i64 + exp;
288    // PG's numeric format: up to 131072 digits before the point, 16383 after.
289    if new_point > 131_072 {
290        return SciExpanded::Overflow;
291    }
292    if (digits.len() as i64 - new_point) > 16_383 {
293        return SciExpanded::Overflow;
294    }
295    let sign = if neg { "-" } else { "" };
296    let plain = if new_point <= 0 {
297        let mut out = String::with_capacity(digits.len() + 2 + (-new_point) as usize);
298        out.push_str("0.");
299        for _ in 0..(-new_point) {
300            out.push('0');
301        }
302        out.push_str(&digits);
303        out
304    } else if (new_point as usize) >= digits.len() {
305        let mut out = digits;
306        for _ in 0..(new_point as usize - out.len()) {
307            out.push('0');
308        }
309        out
310    } else {
311        let mut out = String::with_capacity(digits.len() + 1);
312        out.push_str(&digits[..new_point as usize]);
313        out.push('.');
314        out.push_str(&digits[new_point as usize..]);
315        out
316    };
317    SciExpanded::Expanded(alloc::format!("{sign}{plain}"))
318}
319
320/// v7.39 (round 367, M20) — lower a MySQL hexadecimal binary-string
321/// literal (`0x…` / `X'…'`) onto the existing bytea cast. The hex digits
322/// are left-padded to an even count (`0x123` → byte string `01 23`, per
323/// MariaDB) and handed to the PG bytea input format (`\x…`).
324#[inline(never)]
325fn hex_literal_to_bytea_expr(hex: &str) -> Expr {
326    let padded = if hex.len() % 2 == 1 {
327        alloc::format!("0{hex}")
328    } else {
329        hex.to_string()
330    };
331    Expr::Cast {
332        expr: alloc::boxed::Box::new(Expr::Literal(Literal::String(alloc::format!(
333            "\\x{padded}"
334        )))),
335        target: CastTarget::Named("bytea".to_string()),
336    }
337}
338
339/// v7.39 (round 367, M20) — lower a MySQL bit-value literal (`b'1010'`)
340/// onto the bytea cast. The bits are read big-endian and left-padded to a
341/// whole number of bytes (`b'1010'` → one byte `0x0A`, per MariaDB).
342#[inline(never)]
343fn bits_literal_to_bytea_expr(bits: &str) -> Expr {
344    let pad = (8 - bits.len() % 8) % 8;
345    let mut hex = String::with_capacity((bits.len() + pad).div_ceil(4));
346    let padded: String = core::iter::repeat_n('0', pad).chain(bits.chars()).collect();
347    for nibble in padded.as_bytes().chunks(4) {
348        let mut v = 0u8;
349        for &b in nibble {
350            v = (v << 1) | (b - b'0');
351        }
352        hex.push(char::from_digit(u32::from(v), 16).unwrap_or('0'));
353    }
354    hex_literal_to_bytea_expr(&hex)
355}
356
357/// Resolve a lexer `Token::Numeric` into its literal. PG semantics: a dotted
358/// or over-i64 literal is exact NUMERIC; scientific notation is NUMERIC too
359/// (expanded to the plain decimal form); only a fractional depth beyond SPG's
360/// scale width (u8) falls back to double precision.
361///
362/// v7.38.19 — that sentence used to end "(recorded delta)". Measured
363/// against PG 18.4: a literal with 300 fractional digits round-trips
364/// identically on both engines, so whatever the note described is gone.
365/// It is RD-7 in `docs/RECORDED_DELTAS.md`, under "corrected by
366/// measurement" rather than under "open".
367/// Kept out of the parse_expr recursion frame — see the call site.
368#[inline(never)]
369fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
370    match parse_decimal_literal(&s) {
371        Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
372        // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
373        // its exact value as a NumericBig.
374        None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
375        // v7.39 (read01 numeric.c) — expand the exponent form.
376        None => match expand_scientific_literal(&s) {
377            SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
378                Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
379                None if plain
380                    .split_once('.')
381                    .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
382                {
383                    Ok(Literal::NumericBig(plain))
384                }
385                None => s
386                    .parse::<f64>()
387                    .map(Literal::Float)
388                    .map_err(|_| format!("invalid numeric literal {s:?}")),
389            },
390            SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
391            SciExpanded::NotScientific => s
392                .parse::<f64>()
393                .map(Literal::Float)
394                .map_err(|_| format!("invalid numeric literal {s:?}")),
395        },
396    }
397}
398
399fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
400    let (int_part, frac_part) = match s.split_once('.') {
401        Some((i, f)) => (i, f),
402        None => (s, ""),
403    };
404    // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
405    // places fell out of the numeric path here, which is why
406    // `pg_typeof(1e-256)` answered double precision and a plain
407    // 256-place decimal aborted the query in the big-decimal converter.
408    if frac_part.len() > u16::MAX as usize {
409        return None;
410    }
411    let mut digits = String::with_capacity(int_part.len() + frac_part.len());
412    digits.push_str(int_part);
413    digits.push_str(frac_part);
414    let mantissa: i128 = digits.parse().ok()?;
415    #[allow(clippy::cast_possible_truncation)]
416    Some((mantissa, frac_part.len() as u16))
417}
418
419/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
420/// record-returning JSON functions that take a `AS alias(col type, …)`
421/// column-definition list in FROM position.
422fn is_json_to_record_name(s: &str) -> bool {
423    s.eq_ignore_ascii_case("jsonb_to_recordset")
424        || s.eq_ignore_ascii_case("jsonb_to_record")
425        // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
426        // column-definition list desugars identically (the record base
427        // argument only carries the type; a non-NULL base's field
428        // defaults are a recorded delta, RD-6).
429        || s.eq_ignore_ascii_case("json_populate_record")
430        || s.eq_ignore_ascii_case("jsonb_populate_record")
431        || s.eq_ignore_ascii_case("json_populate_recordset")
432        || s.eq_ignore_ascii_case("jsonb_populate_recordset")
433        || s.eq_ignore_ascii_case("json_to_recordset")
434        || s.eq_ignore_ascii_case("json_to_record")
435}
436
437impl Parser {
438    /// Whether what follows an identifier ends an index key, which is how
439    /// an operator class is told from anything else in that position.
440    fn opclass_position_follows(next: Option<&Token>) -> bool {
441        match next {
442            // `ASC` / `DESC` have their own tokens; matching them as
443            // identifiers named "asc" / "desc" — which the first version of
444            // this did — never fires, and `(c text_pattern_ops DESC)` (which
445            // PG18.4 accepts, verified) went on failing to parse.
446            Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
447            Some(Token::Ident(w)) => {
448                w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
449            }
450            _ => false,
451        }
452    }
453}
454
455fn is_vector_opclass_name(name: &str) -> bool {
456    let lc = name.to_ascii_lowercase();
457    matches!(
458        lc.as_str(),
459        "vector_cosine_ops"
460            | "vector_l2_ops"
461            | "vector_ip_ops"
462            | "halfvec_cosine_ops"
463            | "halfvec_l2_ops"
464            | "halfvec_ip_ops"
465            | "sq8_cosine_ops"
466            | "sq8_l2_ops"
467            | "sq8_ip_ops"
468            // pg_trgm — trigram operator class. SPG's GIN index
469            // already uses tsvector tokens; trigram-style LIKE
470            // pattern matching still routes through a sequential
471            // scan, but the opclass name is accepted so PG schemas
472            // load.
473            | "gin_trgm_ops"
474            | "gist_trgm_ops"
475            // PG built-in btree opclasses occasionally appear in
476            // pg_dump output for column types with multiple
477            // sort orders (text_pattern_ops, varchar_pattern_ops,
478            // bpchar_pattern_ops).
479            | "text_pattern_ops"
480            | "varchar_pattern_ops"
481            | "bpchar_pattern_ops"
482            | "int4_ops"
483            | "int8_ops"
484            | "text_ops"
485    )
486}
487
488#[derive(Debug, Clone, PartialEq, Eq)]
489pub struct ParseError {
490    pub message: String,
491    /// Index into the token stream where parsing tripped. Not a byte offset.
492    /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
493    /// field would grow every `Result<_, ParseError>` slot on the deeply
494    /// recursive parse stack and tip the nesting-budget frame cliff. PG's
495    /// 1-based char position is recovered on the cold error path by
496    /// [`syntax_error_position`], which re-tokenizes to map this token index.
497    pub token_pos: usize,
498}
499
500impl fmt::Display for ParseError {
501    /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
502    /// with `parse error at token #N: `, which PG has no equivalent of:
503    /// the message bodies are already PG's verbatim (`LIMIT must not be
504    /// negative`, `invalid input syntax for type bigint: "abc"`), and the
505    /// prefix was SPG's internal token index leaking into every one of
506    /// them. `token_pos` stays a field — the wire recovers PG's 1-based
507    /// character position from it for the ErrorResponse `P`.
508    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509        f.write_str(&self.message)
510    }
511}
512
513impl From<LexError> for ParseError {
514    fn from(e: LexError) -> Self {
515        Self {
516            message: format!("lex: {e}"),
517            token_pos: 0,
518        }
519    }
520}
521
522/// v7.9.30 — parse a single expression (no trailing junk). Used by
523/// the engine to re-hydrate stored partial-index / unique-index
524/// predicates from their canonical Display form. The same Pratt
525/// parser the statement path uses; this entry point just skips the
526/// statement dispatch.
527pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
528    let (tokens, offsets) = lexer::tokenize_with_offsets(input, lexer::Dialect::PG)
529        .map_err(|e| shape_lex_error(&e, input))?;
530    let mut p = Parser::new(tokens);
531    let expr = p
532        .parse_expr(0)
533        .and_then(|e| p.expect_eof().map(|()| e))
534        .map_err(|e| shape_syntax_error(e, input, &offsets))?;
535    Ok(expr)
536}
537
538/// Parse exactly one statement, swallow an optional trailing `;`, and require
539/// the token stream to end there. PG string semantics.
540pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
541    parse_statement_with(input, lexer::Dialect::PG)
542}
543
544/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
545/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
546/// The engine threads its session flag through here.
547pub fn parse_statement_with(input: &str, dialect: lexer::Dialect) -> Result<Statement, ParseError> {
548    let (tokens, offsets, merges) =
549        lexer::tokenize_with_merges(input, dialect).map_err(|e| shape_lex_error(&e, input))?;
550    // v7.39.2 — the grammar follows "is this MySQL", the lexer follows
551    // "does backslash escape". They used to be one flag, and a session
552    // that turned escapes off lost the grammar with them.
553    let mut p = Parser::new_with_dialect(tokens, dialect.speaks_mysql)
554        .with_source(input, &offsets)
555        .with_merges(merges);
556    let stmt = (|| {
557        let stmt = p.parse_one_statement()?;
558        if matches!(p.peek(), Token::Semicolon) {
559            p.advance();
560        }
561        p.expect_eof()?;
562        Ok(stmt)
563    })()
564    .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
565    Ok(stmt)
566}
567
568/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
569/// `syntax error at or near "<token>"` and `syntax error at end of input`
570/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
571/// prose — `expected identifier, got Eof`, `unexpected token From in
572/// expression`, `expected end of input, got Ident("with")` — which named
573/// internal token types and, in the Debug forms, leaked the parser's own
574/// enum into a message clients read.
575///
576/// Applied once on the way out, so every construction site is covered and
577/// the token named is the one the error itself points at. Messages whose
578/// bodies are already PG's verbatim (`LIMIT must not be negative`,
579/// `invalid input syntax for type bigint: "abc"`) are left alone — those
580/// are PG's own errors, not its syntax error.
581fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
582    if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
583        return e;
584    }
585    let message = match offending_lexeme(input, offsets, e.token_pos) {
586        Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
587        None => "syntax error at end of input".into(),
588    };
589    ParseError {
590        message,
591        token_pos: e.token_pos,
592    }
593}
594
595/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
596/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
597/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
598/// comment at or near "/* x"` — the quoted part runs from the opening
599/// delimiter to the end of the input. SPG reported its own internal
600/// shape instead (`unterminated string literal at byte 7`), which named
601/// a byte offset no client can use.
602fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
603    use lexer::LexErrorKind as K;
604    let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
605    let message = match &e.kind {
606        K::UnterminatedString => {
607            alloc::format!("unterminated quoted string at or near \"{from_here}\"")
608        }
609        K::UnterminatedQuotedIdent => {
610            alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
611        }
612        K::UnterminatedBlockComment => {
613            alloc::format!("unterminated /* comment at or near \"{from_here}\"")
614        }
615        // PG has no "unknown character" error of its own — the character
616        // is skipped and the parser reports the next token. SPG stops at
617        // the character itself and names it, which is the same shape.
618        K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
619        // The number-literal kinds already carry PG's `at or near` form.
620        other => alloc::format!(
621            "{}",
622            lexer::LexError {
623                kind: other.clone(),
624                pos: e.pos,
625            }
626        ),
627    };
628    ParseError {
629        message,
630        token_pos: 0,
631    }
632}
633
634/// The offending token exactly as it appears in the input, or `None` at
635/// end of input. PG echoes the source spelling — a lower-case `frm`
636/// reports as `frm`, not as a canonicalised keyword.
637fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
638    let start = *offsets.get(token_pos)?;
639    if start >= input.len() {
640        return None;
641    }
642    let end = offsets
643        .get(token_pos + 1)
644        .copied()
645        .unwrap_or(input.len())
646        .min(input.len());
647    let seg = input.get(start..end)?.trim();
648    if seg.is_empty() {
649        return None;
650    }
651    // A quoted literal / identifier keeps its inner spaces; anything else
652    // ends at the first whitespace (the segment runs to the NEXT token's
653    // start, which may swallow a comment).
654    if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
655        Some(seg)
656    } else {
657        seg.split_whitespace().next()
658    }
659}
660
661/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
662/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
663/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
664/// this re-tokenizes `input` on the cold error path to map the failing token
665/// index to its start byte, then to a character offset. The dialect
666/// must match the parse that produced `token_pos` (it barely shifts offsets,
667/// but stay consistent). Returns `None` when the index has no offset or the
668/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
669#[must_use]
670pub fn syntax_error_position(
671    input: &str,
672    dialect: lexer::Dialect,
673    token_pos: usize,
674) -> Option<usize> {
675    let (_, offsets) = lexer::tokenize_with_offsets(input, dialect).ok()?;
676    let byte_off = *offsets.get(token_pos)?;
677    if byte_off > input.len() || !input.is_char_boundary(byte_off) {
678        return None;
679    }
680    Some(input[..byte_off].chars().count() + 1)
681}
682
683struct Parser {
684    tokens: Vec<Token>,
685    pos: usize,
686    /// v7.39 (round 274) — the session's dialect, carried by the same
687    /// signal that drives string-literal escaping: `SET sql_mode` (only
688    /// MySQL clients and mysqldump preambles emit it) turns it on,
689    /// `SET standard_conforming_strings` (every pg_dump preamble) turns
690    /// it off. Needed here because the two dialects disagree about what
691    /// `REAL` means — see the type mapping below.
692    mysql_dialect: bool,
693    /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
694    /// mutually recursive expr/select parsers. Bounded so a deeply
695    /// nested input returns a parse error instead of overflowing
696    /// the stack (embed hosts die on overflow — it is an abort,
697    /// not a catchable error).
698    nest_depth: usize,
699    /// TABLESAMPLE lowering channel: the table-ref parser pushes a
700    /// `random() < p/100` predicate here; the enclosing SELECT
701    /// drains the list after its WHERE parses and ANDs the
702    /// predicates in. parse_bare_select save/restores around its
703    /// FROM+WHERE so nested selects only drain their own.
704    pending_sample_preds: Vec<Expr>,
705    /// v7.38.19 — the target of a `SELECT … INTO <table>`, carried out
706    /// of `parse_bare_select` (which returns a `SelectStatement` and has
707    /// nowhere to put it) to the caller that lowers the pair to the CTAS
708    /// node. `bool` is `TEMP`.
709    pending_select_into: Option<(String, bool)>,
710    /// v7.39 (round 691) — collation lowering channel, the same shape as
711    /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
712    /// information, and `ast::OrderBy` is where this parser keeps ordering
713    /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
714    /// variant — puts a new arm on `eval_expr`, which this repo has
715    /// measured to overflow the debug stack. So while an ORDER BY KEY is
716    /// being parsed the postfix loop drops the name here instead of
717    /// refusing it, and the key's parser takes it.
718    ///
719    /// Only inside an ORDER BY key: everywhere else an unperformable
720    /// collation still errors, because accepting one at a COMPARISON and
721    /// ignoring it is the defect F36 exists to close.
722    in_order_by_key: bool,
723    order_key_collation: Option<String>,
724    /// POSITION(sub IN str) — while parsing the needle, the IN
725    /// keyword is the argument separator, not a membership test.
726    /// The postfix loop leaves IN unconsumed when this is set.
727    suppress_in_tail: bool,
728    /// Index of the token the last `advance()` returned — see
729    /// [`Parser::consumed_pos`].
730    last_consumed: usize,
731    /// v7.39 (round 506) — the statement's own text and the byte each token
732    /// starts at, so a MySQL projection item can report the SOURCE TEXT
733    /// MariaDB reports: `SELECT a  +  b` names its column `a  +  b`,
734    /// spacing and all. Only filled for a MySQL session — a PG one names
735    /// columns from the parsed shape and pays nothing for this.
736    src: Option<(String, Vec<usize>)>,
737    /// v7.39.3 — (token index, first-segment byte length) for every
738    /// string literal the lexer built by implicit concatenation.
739    merges: Vec<(usize, usize)>,
740}
741
742/// Max expr/select parser nesting (parens, subqueries, CASE, …).
743/// Real SQL nests a few dozen levels at the extreme. Each nesting level
744/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
745/// exists to turn a deep statement into a catchable parse ERROR: a stack
746/// overflow is an abort, and in the server it does not fail one query, it
747/// takes the process down and every other connection with it.
748///
749/// v7.39 (round 507) — measured, because the figure here used to be a
750/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
751/// in BOTH debug and release"), and the debug half of that is wrong by
752/// more than an order of magnitude:
753///
754///   * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
755///     this budget and errors. Verified against a live server for nested
756///     derived tables, parens, calls, CASE, IN-subqueries, scalar
757///     subqueries, NOT and unary minus — the server stayed up through all
758///     of them. This is the contract that matters, and it holds.
759///   * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
760///     LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
761///     and executing aborts around 8 inside a test thread. The budget is
762///     simply unreachable there, which is why a deep-nesting test has to
763///     ask for a large stack of its own — see `nesting_budget_errors_at`
764///     in the parser tests.
765/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
766/// one place.
767///
768/// There were two copies of this fact: a curated list, used for BARE
769/// names, and — in `try_peek_meta_qualified` — no list at all, which
770/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
771/// the engine to complain about a view it could not materialise. So
772/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
773/// had rows, `pg_catalog.pg_stat_activity` was an error.
774///
775/// PG puts `pg_catalog` at the implicit front of every search_path, so
776/// the two spellings name the same relation and must resolve the same
777/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
778/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
779/// meta_view_result path under their own names and must not be
780/// rewritten; a name that is neither reaches the ordinary resolver,
781/// which reports that the relation does not exist — PG's answer.
782const SYNTHESISED_PG_CATALOGS: &[&str] = &[
783    "pg_am",
784    "pg_attrdef",
785    "pg_attribute",
786    "pg_cast",
787    "pg_db_role_setting",
788    "pg_conversion",
789    "pg_default_acl",
790    "pg_shadow",
791    "pg_sequences",
792    "pg_range",
793    "pg_partitioned_table",
794    "pg_language",
795    "pg_group",
796    "pg_authid",
797    "pg_class",
798    "pg_collation",
799    "pg_constraint",
800    "pg_database",
801    "pg_depend",
802    "pg_amop",
803    "pg_amproc",
804    "pg_opclass",
805    "pg_opfamily",
806    // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
807    "pg_description",
808    "pg_enum",
809    "pg_extension",
810    // v7.39 (round 541) — pg_dump reads it for every relation of kind
811    // 'f'. SPG has no foreign tables, so it is empty, which is also
812    // what PG reports on a database that has none.
813    "pg_foreign_table",
814    // v7.39 (round 541) — the empty-by-truth family; see
815    // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
816    "pg_event_trigger",
817    "pg_file_settings",
818    "pg_foreign_data_wrapper",
819    "pg_foreign_server",
820    "pg_hba_file_rules",
821    "pg_ident_file_mappings",
822    "pg_init_privs",
823    "pg_parameter_acl",
824    "pg_prepared_xacts",
825    "pg_publication_namespace",
826    "pg_publication_rel",
827    "pg_publication_tables",
828    "pg_replication_origin",
829    "pg_replication_origin_status",
830    "pg_seclabel",
831    "pg_seclabels",
832    "pg_shdepend",
833    "pg_shdescription",
834    "pg_shmem_allocations",
835    "pg_shmem_allocations_numa",
836    "pg_shseclabel",
837    "pg_statistic_ext_data",
838    "pg_stats_ext",
839    "pg_stats_ext_exprs",
840    "pg_subscription_rel",
841    "pg_transform",
842    "pg_user_mapping",
843    "pg_user_mappings",
844    "pg_index",
845    "pg_indexes",
846    "pg_inherits",
847    // v7.39 (round 650) — the text-search catalogs SPG can fill
848    // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
849    // token types to dictionaries and SPG has no token-type model,
850    // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
851    "pg_ts_config",
852    "pg_ts_config_map",
853    "pg_ts_dict",
854    "pg_ts_parser",
855    "pg_ts_template",
856    "pg_matviews",
857    "pg_namespace",
858    // v7.39 (round 621)
859    "pg_operator",
860    "pg_policies",
861    "pg_policy",
862    "pg_proc",
863    "pg_publication",
864    "pg_replication_slots",
865    "pg_roles",
866    // v7.39 (round 143) — the rewrite-rule listing view.
867    // v7.39 (round 312) — and the rule catalogue itself, which
868    // `pg_get_ruledef(oid)` resolves against.
869    "pg_rewrite",
870    "pg_rules",
871    "pg_sequence",
872    "pg_settings",
873    "pg_stat_archiver",
874    "pg_stat_bgwriter",
875    "pg_stat_checkpointer",
876    "pg_stat_database",
877    "pg_stat_io",
878    "pg_stat_progress_analyze",
879    "pg_auth_members",
880    "pg_stat_progress_create_index",
881    "pg_stat_progress_vacuum",
882    "pg_stat_replication",
883    "pg_stat_slru",
884    "pg_stat_subscription_stats",
885    "pg_stat_user_functions",
886    "pg_stat_user_indexes",
887    "pg_stat_user_tables",
888    "pg_stat_wal",
889    "pg_prepared_statements",
890    "pg_largeobject",
891    "pg_largeobject_metadata",
892    "pg_statistic",
893    "pg_statistic_ext",
894    // v7.38.18 — the readable view over pg_statistic.
895    "pg_stats",
896    "pg_subscription",
897    "pg_tables",
898    "pg_tablespace",
899    // v7.39 (round 502) — the timezone catalogues. SPG resolved
900    // named zones correctly but could not list them, so a client
901    // populating a timezone picker got "relation does not exist".
902    "pg_timezone_abbrevs",
903    "pg_timezone_names",
904    "pg_trigger",
905    "pg_type",
906    "pg_user",
907    "pg_views",
908];
909
910const MAX_NEST_DEPTH: usize = 64;
911
912/// Stack accounting for the nesting budget, test-only.
913///
914/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
915/// that MOVES: a compiler upgrade grew the parser's debug frames and
916/// silently ate the margin until `nesting_budget_errors_cleanly` went
917/// from erroring cleanly to aborting on a stack overflow. A count
918/// cannot notice that on its own, so the budget is measured here and
919/// held to a ceiling.
920///
921/// The reading has to come from a helper whose OWN frame is the same at
922/// every call: debug slot placement does not follow source order, so a
923/// local's address inside the function under test is not that
924/// function's frame boundary. Two earlier probes were wrong that way —
925/// one read `&self.nest_depth`, which is the `Parser`'s address and
926/// never moves at all.
927#[cfg(test)]
928mod frame_meter {
929    extern crate std;
930    use std::cell::Cell;
931
932    // Per-THREAD, not global. `cargo test` runs tests in parallel and
933    // plenty of them parse nested expressions, so shared statics get
934    // stack addresses from several threads at once and the subtraction
935    // below turns into noise — it read 229,772 bytes per level that way,
936    // while passing when the test was run on its own.
937    std::thread_local! {
938        static AT_LO: Cell<usize> = const { Cell::new(0) };
939        static AT_HI: Cell<usize> = const { Cell::new(0) };
940    }
941
942    pub(super) const SAMPLE_LO: usize = 4;
943    pub(super) const SAMPLE_HI: usize = 24;
944
945    #[inline(never)]
946    pub(super) fn record(depth: usize) {
947        let anchor = 0u8;
948        let at = core::ptr::from_ref(&anchor) as usize;
949        if depth == SAMPLE_LO {
950            AT_LO.with(|c| c.set(at));
951        } else if depth == SAMPLE_HI {
952            AT_HI.with(|c| c.set(at));
953        }
954    }
955
956    /// Bytes of stack one nesting level costs, averaged over the span.
957    pub(super) fn bytes_per_level() -> usize {
958        let lo = AT_LO.with(Cell::get);
959        let hi = AT_HI.with(Cell::get);
960        assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
961        assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
962        (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
963    }
964
965    pub(super) fn reset() {
966        AT_LO.with(|c| c.set(0));
967        AT_HI.with(|c| c.set(0));
968    }
969}
970
971/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
972/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
973#[inline(never)]
974fn build_center_call(e: Expr) -> Expr {
975    Expr::FunctionCall {
976        name: alloc::string::String::from("center"),
977        args: alloc::vec![e],
978    }
979}
980
981/// Max consecutive binary operators at ONE precedence level
982/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
983/// parse time but evaluates and drops recursively — depth beyond
984/// this overflows 2 MiB worker stacks (debug eval frames run
985/// multiple KiB). `IN (…)` lists are flat and unaffected.
986const MAX_BINARY_CHAIN: usize = 256;
987
988/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
989/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
990/// it keeps its dedicated path (`parse_table_level_fk`).
991enum NamedTableConstraintKind {
992    Check,
993    Unique,
994    PrimaryKey,
995    Exclude,
996}
997
998impl Parser {
999    fn new(tokens: Vec<Token>) -> Self {
1000        Self::new_with_dialect(tokens, false)
1001    }
1002
1003    fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
1004        Self {
1005            tokens,
1006            mysql_dialect,
1007            in_order_by_key: false,
1008            order_key_collation: None,
1009            pos: 0,
1010            nest_depth: 0,
1011            pending_sample_preds: Vec::new(),
1012            pending_select_into: None,
1013            suppress_in_tail: false,
1014            last_consumed: 0,
1015            src: None,
1016            merges: Vec::new(),
1017        }
1018    }
1019
1020    /// Hand the parser the text it is parsing, for [`Parser::source_span`].
1021    fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
1022        if self.mysql_dialect {
1023            self.src = Some((input.to_string(), offsets.to_vec()));
1024        }
1025        self
1026    }
1027
1028    /// v7.39.3 — the implicit-concatenation log from the lexer, so a
1029    /// merged literal can still be LABELLED by its first segment the way
1030    /// MySQL 9.7.2 labels it.
1031    fn with_merges(mut self, merges: Vec<(usize, usize)>) -> Self {
1032        if self.mysql_dialect {
1033            self.merges = merges;
1034        }
1035        self
1036    }
1037
1038    /// The byte length of the first segment of the literal at `tok`, when
1039    /// that literal was built by implicit concatenation.
1040    fn merged_first_len(&self, tok: usize) -> Option<usize> {
1041        self.merges
1042            .iter()
1043            .find(|(k, _)| *k == tok)
1044            .map(|(_, len)| *len)
1045    }
1046
1047    /// The source text spanning tokens `start ..= end`, trimmed.
1048    ///
1049    /// The offsets are token STARTS, so the span runs to the start of the
1050    /// token after `end` and gives back the whitespace between them —
1051    /// trimming is what makes `a + b FROM t` end at `b`.
1052    fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1053        let (text, offsets) = self.src.as_ref()?;
1054        let from = *offsets.get(start)?;
1055        let to = *offsets.get(end + 1)?;
1056        text.get(from..to).map(str::trim_end)
1057    }
1058
1059    /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1060    /// nesting depth, erroring out cleanly past the budget.
1061    fn enter_nested(&mut self) -> Result<(), ParseError> {
1062        self.nest_depth += 1;
1063        #[cfg(test)]
1064        frame_meter::record(self.nest_depth);
1065        if self.nest_depth > MAX_NEST_DEPTH {
1066            self.nest_depth -= 1;
1067            return Err(self.err(alloc::format!(
1068                "statement nests deeper than {MAX_NEST_DEPTH} levels"
1069            )));
1070        }
1071        Ok(())
1072    }
1073
1074    fn peek(&self) -> &Token {
1075        // tokens always ends with Eof; pos is clamped in advance().
1076        &self.tokens[self.pos]
1077    }
1078
1079    fn advance(&mut self) -> Token {
1080        let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1081        self.last_consumed = self.pos;
1082        if self.pos + 1 < self.tokens.len() {
1083            self.pos += 1;
1084        }
1085        t
1086    }
1087
1088    /// v7.39 (round 340, V56) — the index of the token `advance()` just
1089    /// returned. It was computed as `pos - 1`, which is wrong at both
1090    /// ends: `advance()` parks on the final Eof rather than running off
1091    /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1092    /// input`), and after backtracking `pos` is no longer one past the
1093    /// token that failed. Recorded by `advance()` itself instead.
1094    const fn consumed_pos(&self) -> usize {
1095        self.last_consumed
1096    }
1097
1098    fn err(&self, message: String) -> ParseError {
1099        ParseError {
1100            message,
1101            token_pos: self.pos,
1102        }
1103    }
1104
1105    /// v7.39.3 — like [`Parser::err`] but pointing at a token the caller
1106    /// names rather than at the current one.
1107    ///
1108    /// The position is not decoration on the MySQL wire: its syntax-error
1109    /// sentence quotes the source from there to the end of the statement,
1110    /// so an error raised after the construct it is about quotes nothing.
1111    fn err_at(&self, token_pos: usize, message: String) -> ParseError {
1112        ParseError { message, token_pos }
1113    }
1114
1115    fn expect_eof(&self) -> Result<(), ParseError> {
1116        if matches!(self.peek(), Token::Eof) {
1117            Ok(())
1118        } else {
1119            Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1120        }
1121    }
1122
1123    /// v7.14.0 — swallow every token up to (but not including) the
1124    /// next semicolon / EOF. Used by the dump-noise dispatcher
1125    /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1126    /// etc. without modeling each grammar.
1127    /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1128    /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1129    /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1130    /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1131    /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1132    fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1133        let start = self.pos;
1134        self.advance(); // COMMENT
1135        if !matches!(self.peek(), Token::On) {
1136            self.pos = start;
1137            self.consume_until_statement_boundary();
1138            return Ok(Statement::Empty);
1139        }
1140        self.advance(); // ON
1141        let kind = match self.peek() {
1142            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1143            Token::Table => "table".into(),
1144            _ => {
1145                self.consume_until_statement_boundary();
1146                return Ok(Statement::Empty);
1147            }
1148        };
1149        if !matches!(
1150            kind.as_str(),
1151            "table"
1152                | "column"
1153                | "index"
1154                | "view"
1155                | "sequence"
1156                | "schema"
1157                | "type"
1158                | "database"
1159                | "function"
1160        ) {
1161            self.consume_until_statement_boundary();
1162            return Ok(Statement::Empty);
1163        }
1164        self.advance(); // the kind keyword
1165        // The object name. ⚠️ `expect_ident_like` strips a leading
1166        // `<schema>.` qualifier and returns only the trailing ident (SPG is
1167        // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1168        // `c`. Read the dotted parts from raw tokens instead, then let a
1169        // 3-part `schema.t.c` drop its leading schema like everywhere else.
1170        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1171        loop {
1172            match self.advance() {
1173                Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1174                other if unreserved_keyword_text(&other).is_some() => {
1175                    parts.push(unreserved_keyword_text(&other).unwrap());
1176                }
1177                other => {
1178                    return Err(ParseError {
1179                        message: alloc::format!("expected identifier, got {other:?}"),
1180                        token_pos: self.consumed_pos(),
1181                    });
1182                }
1183            }
1184            if matches!(self.peek(), Token::Dot) {
1185                self.advance();
1186            } else {
1187                break;
1188            }
1189        }
1190        // COLUMN wants `table.column`; every other kind wants a bare name.
1191        let want = if kind == "column" { 2 } else { 1 };
1192        while parts.len() > want {
1193            parts.remove(0);
1194        }
1195        let name = parts.join(".");
1196        // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1197        // pg_dump writes the SIGNATURE, and the paren list was a syntax
1198        // error here — a dump carrying one function comment failed to
1199        // restore. The list is consumed (the comment store keys by name;
1200        // overload-precise comments are the function-predicate follow-up).
1201        if matches!(self.peek(), Token::LParen)
1202            && matches!(
1203                kind.as_str(),
1204                "function" | "procedure" | "aggregate" | "routine"
1205            )
1206        {
1207            let mut depth = 0usize;
1208            loop {
1209                match self.advance() {
1210                    Token::LParen => depth += 1,
1211                    Token::RParen => {
1212                        depth -= 1;
1213                        if depth == 0 {
1214                            break;
1215                        }
1216                    }
1217                    Token::Eof => {
1218                        return Err(self.err(alloc::string::String::from(
1219                            "unterminated argument list in COMMENT ON",
1220                        )));
1221                    }
1222                    _ => {}
1223                }
1224            }
1225        }
1226        // `IS`
1227        if !matches!(self.peek(), Token::Is) {
1228            self.expect_keyword_ident("is")?;
1229        } else {
1230            self.advance();
1231        }
1232        let comment = match self.peek() {
1233            Token::Null => {
1234                self.advance();
1235                None
1236            }
1237            _ => Some(self.expect_string_literal()?),
1238        };
1239        Ok(Statement::CommentOn {
1240            kind,
1241            name,
1242            comment,
1243        })
1244    }
1245
1246    /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1247    /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1248    /// [CASCADE|RESTRICT]`.
1249    ///
1250    /// TABLE privileges are the real ones (stored, enforced, introspectable).
1251    /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1252    /// and the no-ON `GRANT role TO role` membership form — parses into
1253    /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1254    /// on them still restores.
1255    fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1256        self.advance(); // GRANT / REVOKE
1257        // REVOKE's optional `GRANT OPTION FOR` prefix.
1258        let mut grant_option = false;
1259        if !grant
1260            && self.peek_keyword_ident("grant")
1261            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1262        {
1263            self.advance(); // GRANT
1264            self.advance(); // OPTION
1265            self.expect_keyword_ident("for")?;
1266            grant_option = true;
1267        }
1268        // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1269        // words each with an optional COLUMN list.
1270        let mut privileges: Vec<GrantPriv> = Vec::new();
1271        if matches!(self.peek(), Token::All) {
1272            self.advance();
1273            if self.peek_keyword_ident("privileges") {
1274                self.advance();
1275            }
1276            // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1277            // column only.
1278            if matches!(self.peek(), Token::LParen) {
1279                let columns = self.parse_grant_column_list()?;
1280                privileges.push(GrantPriv {
1281                    word: "ALL".into(),
1282                    columns,
1283                });
1284            }
1285        } else {
1286            loop {
1287                // SELECT and INSERT lex as reserved tokens, so they never
1288                // reach `expect_ident_like` as plain idents; the rest
1289                // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1290                // MAINTAIN) are ordinary identifiers.
1291                let w = match self.peek() {
1292                    Token::Select => {
1293                        self.advance();
1294                        "SELECT".to_string()
1295                    }
1296                    Token::Insert => {
1297                        self.advance();
1298                        "INSERT".to_string()
1299                    }
1300                    // v7.39 (read01 round 60) — CREATE is a privilege word on a
1301                    // schema / database, and it lexes as a reserved token.
1302                    Token::Create => {
1303                        self.advance();
1304                        "CREATE".to_string()
1305                    }
1306                    // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1307                    // alice`) these "privilege words" are ROLE NAMES, and a
1308                    // role name is case-sensitive. `priv_from_word` folds case
1309                    // itself when they really are privileges.
1310                    _ => self.expect_ident_like()?,
1311                };
1312                // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1313                // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1314                let columns = if matches!(self.peek(), Token::LParen) {
1315                    self.parse_grant_column_list()?
1316                } else {
1317                    Vec::new()
1318                };
1319                privileges.push(GrantPriv { word: w, columns });
1320                if matches!(self.peek(), Token::Comma) {
1321                    self.advance();
1322                } else {
1323                    break;
1324                }
1325            }
1326        }
1327        // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1328        // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1329        if !matches!(self.peek(), Token::On) {
1330            let roles: Vec<String> = core::mem::take(&mut privileges)
1331                .into_iter()
1332                .map(|p| p.word)
1333                .collect();
1334            let grantees = self.parse_grantee_list(grant)?;
1335            // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1336            // no admin-option layer: a member cannot re-grant).
1337            self.consume_until_statement_boundary();
1338            return Ok(finish_grant(
1339                grant,
1340                GrantStatement {
1341                    privileges: Vec::new(),
1342                    object: GrantObject::Roles(roles),
1343                    grantees,
1344                    grant_option,
1345                },
1346            ));
1347        }
1348        self.advance(); // ON
1349        // An optional object-class keyword. `TABLE` (or no keyword at all) is
1350        // the enforced case; anything else parses and no-ops.
1351        let mut class = "TABLE";
1352        match self.peek() {
1353            Token::Table => {
1354                self.advance();
1355            }
1356            Token::All => {
1357                // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1358                // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1359                // IN SCHEMA` stay no-ops and keep their own object class.
1360                self.advance(); // ALL
1361                let kind = match self.peek() {
1362                    Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1363                    // TABLES has its own token (SHOW TABLES owns it).
1364                    Token::Tables | Token::Table => "tables".to_string(),
1365                    _ => String::new(),
1366                };
1367                if !kind.is_empty() {
1368                    self.advance();
1369                }
1370                // `IN SCHEMA <name>`
1371                if matches!(self.peek(), Token::In) {
1372                    self.advance();
1373                    if self.peek_keyword_ident("schema") {
1374                        self.advance();
1375                        let _schema = self.expect_ident_like()?;
1376                    }
1377                }
1378                if kind != "tables" {
1379                    self.consume_until_statement_boundary();
1380                    return Ok(finish_grant(
1381                        grant,
1382                        GrantStatement {
1383                            privileges,
1384                            object: GrantObject::Other("ALL … IN SCHEMA".into()),
1385                            grantees: Vec::new(),
1386                            grant_option,
1387                        },
1388                    ));
1389                }
1390                let grantees = self.parse_grantee_list(grant)?;
1391                self.consume_until_statement_boundary();
1392                return Ok(finish_grant(
1393                    grant,
1394                    GrantStatement {
1395                        privileges,
1396                        object: GrantObject::AllTablesInSchema,
1397                        grantees,
1398                        grant_option,
1399                    },
1400                ));
1401            }
1402            Token::Ident(w) | Token::QuotedIdent(w) => {
1403                let lc = w.to_ascii_lowercase();
1404                // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1405                // real objects with real ACLs now.
1406                if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1407                    self.advance();
1408                    let mut names: Vec<String> = Vec::new();
1409                    loop {
1410                        let mut parts: Vec<String> = Vec::new();
1411                        loop {
1412                            parts.push(self.expect_ident_like()?);
1413                            if matches!(self.peek(), Token::Dot) {
1414                                self.advance();
1415                            } else {
1416                                break;
1417                            }
1418                        }
1419                        names.push(parts.pop().expect("at least one part"));
1420                        if matches!(self.peek(), Token::Comma) {
1421                            self.advance();
1422                        } else {
1423                            break;
1424                        }
1425                    }
1426                    let grantees = self.parse_grantee_list(grant)?;
1427                    let mut grant_option = grant_option;
1428                    if grant && self.peek_keyword_ident("with") {
1429                        self.advance();
1430                        self.expect_keyword_ident("grant")?;
1431                        self.expect_keyword_ident("option")?;
1432                        grant_option = true;
1433                    }
1434                    self.consume_until_statement_boundary();
1435                    let object = match lc.as_str() {
1436                        "sequence" => GrantObject::Sequences(names),
1437                        "schema" => GrantObject::Schemas(names),
1438                        _ => GrantObject::Databases(names),
1439                    };
1440                    return Ok(finish_grant(
1441                        grant,
1442                        GrantStatement {
1443                            privileges,
1444                            object,
1445                            grantees,
1446                            grant_option,
1447                        },
1448                    ));
1449                }
1450                // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1451                // keys functions by NAME, so the argument list parses and is
1452                // dropped (an overload set shares one ACL — recorded residual).
1453                if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1454                    self.advance();
1455                    let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1456                    loop {
1457                        let mut parts: Vec<String> = Vec::new();
1458                        loop {
1459                            parts.push(self.expect_ident_like()?);
1460                            if matches!(self.peek(), Token::Dot) {
1461                                self.advance();
1462                            } else {
1463                                break;
1464                            }
1465                        }
1466                        let fname = parts.pop().expect("at least one part");
1467                        // v7.39 (read01 round 62) — the signature picks the
1468                        // overload, so it is captured.
1469                        let sig = if matches!(self.peek(), Token::LParen) {
1470                            Some(self.parse_function_signature_types()?)
1471                        } else {
1472                            None
1473                        };
1474                        names.push((fname, sig));
1475                        if matches!(self.peek(), Token::Comma) {
1476                            self.advance();
1477                        } else {
1478                            break;
1479                        }
1480                    }
1481                    let grantees = self.parse_grantee_list(grant)?;
1482                    self.consume_until_statement_boundary();
1483                    return Ok(finish_grant(
1484                        grant,
1485                        GrantStatement {
1486                            privileges,
1487                            object: GrantObject::Functions(names),
1488                            grantees,
1489                            grant_option,
1490                        },
1491                    ));
1492                }
1493                if matches!(
1494                    lc.as_str(),
1495                    "type"
1496                        | "domain"
1497                        | "language"
1498                        | "tablespace"
1499                        | "large"
1500                        | "foreign"
1501                        | "parameter"
1502                ) {
1503                    self.consume_until_statement_boundary();
1504                    return Ok(finish_grant(
1505                        grant,
1506                        GrantStatement {
1507                            privileges,
1508                            object: GrantObject::Other(lc.to_ascii_uppercase()),
1509                            grantees: Vec::new(),
1510                            grant_option,
1511                        },
1512                    ));
1513                }
1514                class = "TABLE";
1515            }
1516            _ => {}
1517        }
1518        let _ = class;
1519        // The table list. Schema-qualified names drop their qualifier (SPG is
1520        // single-schema) — but read the dotted parts from raw tokens, since
1521        // `expect_ident_like` would silently swallow the leading part.
1522        let mut tables: Vec<String> = Vec::new();
1523        loop {
1524            let mut parts: Vec<String> = Vec::new();
1525            loop {
1526                parts.push(self.expect_ident_like()?);
1527                if matches!(self.peek(), Token::Dot) {
1528                    self.advance();
1529                } else {
1530                    break;
1531                }
1532            }
1533            tables.push(parts.pop().expect("at least one part"));
1534            if matches!(self.peek(), Token::Comma) {
1535                self.advance();
1536            } else {
1537                break;
1538            }
1539        }
1540        let grantees = self.parse_grantee_list(grant)?;
1541        if grant && self.peek_keyword_ident("with") {
1542            self.advance();
1543            self.expect_keyword_ident("grant")?;
1544            self.expect_keyword_ident("option")?;
1545            grant_option = true;
1546        }
1547        // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1548        // to cascade to (no re-granting), so both are accepted and ignored.
1549        if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1550            self.advance();
1551        }
1552        Ok(finish_grant(
1553            grant,
1554            GrantStatement {
1555                privileges,
1556                object: GrantObject::Tables(tables),
1557                grantees,
1558                grant_option,
1559            },
1560        ))
1561    }
1562
1563    /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1564    /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1565    /// words; the caller normalises them into a signature key.
1566    fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1567        self.advance(); // (
1568        let mut types: Vec<String> = Vec::new();
1569        if matches!(self.peek(), Token::RParen) {
1570            self.advance();
1571            return Ok(types);
1572        }
1573        loop {
1574            // Collect the words of one argument up to a comma / close paren.
1575            let mut words: Vec<String> = Vec::new();
1576            loop {
1577                match self.peek() {
1578                    Token::Comma | Token::RParen | Token::Eof => break,
1579                    _ => {}
1580                }
1581                let tok = self.advance();
1582                match tok {
1583                    Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1584                    other => {
1585                        if let Some(w) = unreserved_keyword_text(&other) {
1586                            words.push(w);
1587                        }
1588                    }
1589                }
1590            }
1591            // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1592            // themselves several words (`double precision`, `character
1593            // varying`, `timestamp with time zone`), so "two words means the
1594            // first is a parameter name" reads the type off `f(double
1595            // precision)` as `precision`. v7.39 (round 282): recognise the
1596            // multi-word spellings first — a leading word that STARTS one of
1597            // them is part of the type, not a name.
1598            let joined = words.join(" ");
1599            let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1600                joined
1601            } else if words.len() >= 2 {
1602                words[1..].join(" ")
1603            } else {
1604                words.first().cloned().unwrap_or_default()
1605            };
1606            types.push(ty);
1607            if matches!(self.peek(), Token::Comma) {
1608                self.advance();
1609            } else {
1610                break;
1611            }
1612        }
1613        if matches!(self.peek(), Token::RParen) {
1614            self.advance();
1615        }
1616        Ok(types)
1617    }
1618
1619    /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1620    fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1621        self.advance(); // (
1622        let mut cols = Vec::new();
1623        loop {
1624            cols.push(self.expect_ident_like()?);
1625            if matches!(self.peek(), Token::Comma) {
1626                self.advance();
1627            } else {
1628                break;
1629            }
1630        }
1631        if !matches!(self.peek(), Token::RParen) {
1632            return Err(self.err(alloc::format!(
1633                "expected ')' to close the column list, got {:?}",
1634                self.peek()
1635            )));
1636        }
1637        self.advance(); // )
1638        Ok(cols)
1639    }
1640
1641    /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1642    /// PUBLIC.
1643    fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1644        if grant {
1645            if matches!(self.peek(), Token::To) {
1646                self.advance();
1647            } else {
1648                self.expect_keyword_ident("to")?;
1649            }
1650        } else if matches!(self.peek(), Token::From) {
1651            self.advance();
1652        } else {
1653            self.expect_keyword_ident("from")?;
1654        }
1655        let mut grantees: Vec<String> = Vec::new();
1656        loop {
1657            // `GROUP name` is the legacy spelling of a plain role name.
1658            if self.peek_keyword_ident("group") {
1659                self.advance();
1660            }
1661            if self.peek_keyword_ident("public") {
1662                self.advance();
1663                grantees.push(String::new()); // PUBLIC
1664            } else {
1665                grantees.push(self.expect_ident_like()?);
1666            }
1667            if matches!(self.peek(), Token::Comma) {
1668                self.advance();
1669            } else {
1670                break;
1671            }
1672        }
1673        Ok(grantees)
1674    }
1675
1676    /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1677    /// The body keeps its `$N` placeholders; substitution happens at
1678    /// EXECUTE. The declared types are recorded for
1679    /// `pg_prepared_statements.parameter_types` but are not enforced —
1680    /// PG infers when the list is omitted, and SPG resolves the values
1681    /// at substitution time either way.
1682    fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1683        let start = self.pos;
1684        self.advance(); // PREPARE
1685        // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1686        // different statement that happens to share the keyword. PG
1687        // ships with `max_prepared_transactions = 0` and reports it
1688        // this way; SPG has no prepared-transaction registry, so the
1689        // same wording is the accurate answer rather than a dodge.
1690        // Round 277 turned this from a silent no-op into a confusing
1691        // "expected AS in PREPARE" parse error.
1692        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1693            self.advance();
1694            let gid = match self.advance() {
1695                Token::String(g) => g,
1696                other => {
1697                    return Err(self.err(alloc::format!(
1698                        "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1699                    )));
1700                }
1701            };
1702            return Ok(Statement::PrepareTransaction(gid));
1703        }
1704        let name = self.expect_ident_like()?;
1705        let mut param_types = Vec::new();
1706        if matches!(self.peek(), Token::LParen) {
1707            self.advance();
1708            loop {
1709                let mut ty = self.expect_ident_like()?;
1710                // A parameterised type name (`numeric(10,2)`,
1711                // `varchar(20)`) keeps its argument list in the text.
1712                if matches!(self.peek(), Token::LParen) {
1713                    let mut depth = 0usize;
1714                    let mut buf = String::from("(");
1715                    loop {
1716                        match self.advance() {
1717                            Token::LParen => {
1718                                depth += 1;
1719                                if depth > 1 {
1720                                    buf.push('(');
1721                                }
1722                            }
1723                            Token::RParen => {
1724                                depth -= 1;
1725                                buf.push(')');
1726                                if depth == 0 {
1727                                    break;
1728                                }
1729                            }
1730                            Token::Comma => buf.push(','),
1731                            Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1732                            Token::Eof => break,
1733                            _ => {}
1734                        }
1735                    }
1736                    ty.push_str(&buf);
1737                }
1738                // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1739                // position, same family as the parameter list above.
1740                let array_suffix = self.consume_array_suffix();
1741                ty.push_str(&array_suffix);
1742                param_types.push(ty);
1743                match self.peek() {
1744                    Token::Comma => {
1745                        self.advance();
1746                    }
1747                    Token::RParen => {
1748                        self.advance();
1749                        break;
1750                    }
1751                    other => {
1752                        return Err(self.err(alloc::format!(
1753                            "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1754                        )));
1755                    }
1756                }
1757            }
1758        }
1759        if !matches!(self.peek(), Token::As) {
1760            return Err(self.err(alloc::format!(
1761                "expected AS in PREPARE, got {:?}",
1762                self.peek()
1763            )));
1764        }
1765        self.advance();
1766        let body = self.parse_one_statement()?;
1767        // The Parser holds tokens, not the source text, so the
1768        // statement PG reports in `pg_prepared_statements.statement`
1769        // is rebuilt from the AST rather than sliced from the input.
1770        let _ = start;
1771        let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1772        if !param_types.is_empty() {
1773            source.push_str(" (");
1774            source.push_str(&param_types.join(", "));
1775            source.push(')');
1776        }
1777        source.push_str(" AS ");
1778        source.push_str(&alloc::format!("{body}"));
1779        Ok(Statement::Prepare {
1780            name,
1781            param_types,
1782            body: alloc::boxed::Box::new(body),
1783            source,
1784        })
1785    }
1786
1787    /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1788    fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1789        self.advance(); // EXECUTE
1790        let name = self.expect_ident_like()?;
1791        let mut args = Vec::new();
1792        if matches!(self.peek(), Token::LParen) {
1793            self.advance();
1794            if matches!(self.peek(), Token::RParen) {
1795                self.advance();
1796            } else {
1797                loop {
1798                    args.push(self.parse_expr(0)?);
1799                    match self.advance() {
1800                        Token::Comma => {}
1801                        Token::RParen => break,
1802                        other => {
1803                            return Err(self.err(alloc::format!(
1804                                "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1805                            )));
1806                        }
1807                    }
1808                }
1809            }
1810        }
1811        Ok(Statement::Execute { name, args })
1812    }
1813
1814    /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1815    /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1816    /// procedure catalog yet, so this reports PG's not-found error
1817    /// (with its HINT) rather than pretending the call ran.
1818    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1819    /// Bare `DISCARD` is a syntax error in PG; so it is here.
1820    fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1821        self.advance(); // DISCARD
1822        let target = match self.advance() {
1823            Token::All => DiscardTarget::All,
1824            Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1825                "all" => DiscardTarget::All,
1826                "plans" => DiscardTarget::Plans,
1827                "sequences" => DiscardTarget::Sequences,
1828                "temp" | "temporary" => DiscardTarget::Temp,
1829                other => {
1830                    return Err(self.err(format!(
1831                        "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1832                    )));
1833                }
1834            },
1835            other => {
1836                return Err(self.err(format!(
1837                    "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1838                )));
1839            }
1840        };
1841        Ok(Statement::Discard(target))
1842    }
1843
1844    /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1845    /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1846    /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1847    /// aggressively the server interrupts, which SPG does not distinguish.
1848    /// Bare `KILL <id>` means CONNECTION.
1849    fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1850        self.advance(); // KILL
1851        let mut query_only = false;
1852        loop {
1853            // CONNECTION is a reserved keyword token (it also opens
1854            // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1855            // `Token::Connection` rather than a bare ident.
1856            if matches!(self.peek(), Token::Connection) {
1857                self.advance();
1858                break;
1859            }
1860            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1861                break;
1862            };
1863            match w.to_ascii_lowercase().as_str() {
1864                "hard" | "soft" => {
1865                    self.advance();
1866                }
1867                "query" => {
1868                    self.advance();
1869                    query_only = true;
1870                    break;
1871                }
1872                _ => break,
1873            }
1874        }
1875        let id = self.parse_expr(0)?;
1876        Ok(Statement::Kill {
1877            query_only,
1878            id: Box::new(id),
1879        })
1880    }
1881
1882    fn parse_call(&mut self) -> Result<Statement, ParseError> {
1883        self.advance(); // CALL
1884        let name = self.expect_ident_like()?;
1885        self.consume_until_statement_boundary();
1886        Ok(Statement::Call(name))
1887    }
1888
1889    fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1890        self.advance(); // DEALLOCATE
1891        // PG accepts an optional noise `PREPARE` keyword here.
1892        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1893            self.advance();
1894        }
1895        if matches!(self.peek(), Token::All) {
1896            self.advance();
1897            return Ok(Statement::Deallocate(None));
1898        }
1899        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1900            self.advance();
1901            return Ok(Statement::Deallocate(None));
1902        }
1903        let name = self.expect_ident_like()?;
1904        Ok(Statement::Deallocate(Some(name)))
1905    }
1906
1907    fn consume_until_statement_boundary(&mut self) {
1908        loop {
1909            match self.peek() {
1910                Token::Semicolon | Token::Eof => return,
1911                _ => self.advance(),
1912            };
1913        }
1914    }
1915
1916    /// v7.38.19 — the database name a `CREATE DATABASE` names, skipping
1917    /// an `IF NOT EXISTS`. Consumes only the name; the collation scanner
1918    /// runs after it and eats the rest.
1919    fn scan_database_name(&mut self) -> Option<String> {
1920        // The caller has only PEEKED at `DATABASE`; step past it, or the
1921        // first identifier found is the keyword itself. It was, and
1922        // `pg_database` listed a database called `database`.
1923        if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case("database"))
1924        {
1925            self.advance();
1926        }
1927        for kw in ["if", "not", "exists"] {
1928            if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case(kw))
1929            {
1930                self.advance();
1931            }
1932        }
1933        match self.peek().clone() {
1934            Token::Ident(w) | Token::QuotedIdent(w) => {
1935                self.advance();
1936                Some(w)
1937            }
1938            _ => None,
1939        }
1940    }
1941
1942    /// v7.38.18 — consume to the statement boundary like
1943    /// `consume_until_statement_boundary`, but pick out the collation a
1944    /// `CREATE DATABASE` asked for on the way.
1945    ///
1946    /// `LC_COLLATE 'de_DE.utf8'` and `LOCALE 'de_DE.utf8'` both count;
1947    /// `LC_CTYPE` does not, because SPG has no separate ctype and
1948    /// pretending to honour it would be the more misleading answer. An
1949    /// `=` between the keyword and the value is optional, as in PG.
1950    ///
1951    /// The whole statement used to be thrown away. Being single-database
1952    /// makes the NAME a no-op; it does not make the collation one.
1953    fn scan_database_collation_until_boundary(&mut self) -> Option<String> {
1954        let mut want_value = false;
1955        let mut found: Option<String> = None;
1956        loop {
1957            let tok = self.peek().clone();
1958            match &tok {
1959                Token::Semicolon | Token::Eof => break,
1960                Token::Ident(w) | Token::QuotedIdent(w)
1961                    if w.eq_ignore_ascii_case("lc_collate") || w.eq_ignore_ascii_case("locale") =>
1962                {
1963                    want_value = true;
1964                }
1965                Token::Eq if want_value => {}
1966                Token::String(v) if want_value => {
1967                    found = Some(v.clone());
1968                    want_value = false;
1969                }
1970                Token::Ident(v) | Token::QuotedIdent(v) if want_value => {
1971                    found = Some(v.clone());
1972                    want_value = false;
1973                }
1974                _ => want_value = false,
1975            }
1976            self.advance();
1977        }
1978        found
1979    }
1980
1981    /// v7.22 (round-13 T2) — consume to the statement boundary like
1982    /// `consume_until_statement_boundary`, but pick out the sequence
1983    /// name on the way: either `SEQUENCE NAME <ident>` (identity
1984    /// columns) or the first string literal (`nextval('<seq>')`).
1985    /// Schema qualifiers and `::regclass` casts are stripped.
1986    fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1987        let mut seq: Option<String> = None;
1988        let mut after_sequence_kw = false;
1989        let mut after_name_kw = false;
1990        loop {
1991            match self.peek().clone() {
1992                Token::Semicolon | Token::Eof => break,
1993                Token::Ident(s) | Token::QuotedIdent(s) => {
1994                    if after_name_kw && seq.is_none() {
1995                        self.advance();
1996                        let mut name = s;
1997                        // `SEQUENCE NAME public.groups_id_seq` — keep
1998                        // the bare name, drop qualifiers.
1999                        while matches!(self.peek(), Token::Dot) {
2000                            self.advance();
2001                            if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
2002                                name = n;
2003                            }
2004                        }
2005                        seq = Some(name);
2006                        after_name_kw = false;
2007                        continue;
2008                    }
2009                    if after_sequence_kw && s.eq_ignore_ascii_case("name") {
2010                        after_name_kw = true;
2011                        after_sequence_kw = false;
2012                    } else {
2013                        after_sequence_kw = s.eq_ignore_ascii_case("sequence");
2014                    }
2015                    self.advance();
2016                }
2017                Token::String(s) => {
2018                    if seq.is_none() {
2019                        // `nextval('public.groups_id_seq'::regclass)`
2020                        let bare = s
2021                            .rsplit_once('.')
2022                            .map_or_else(|| s.clone(), |(_, b)| b.to_string());
2023                        seq = Some(bare);
2024                    }
2025                    self.advance();
2026                }
2027                _ => {
2028                    after_sequence_kw = false;
2029                    after_name_kw = false;
2030                    self.advance();
2031                }
2032            }
2033        }
2034        seq
2035    }
2036
2037    /// v7.39 (round 621) — is the next token the keyword `BY`?
2038    ///
2039    /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
2040    /// column, table and alias name — and SPG lexed it into a dedicated
2041    /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
2042    /// two-letter keywords the lexer knew, this was the only one PG leaves
2043    /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
2044    ///
2045    /// The token is gone; the three clauses that own the word — GROUP BY,
2046    /// ORDER BY, PARTITION BY — and the handful of other places that expect it
2047    /// ask this instead. Adding it to the unreserved-identifier table was not
2048    /// enough on its own: identifier positions that match the token shape
2049    /// directly (an index's column list, a table alias) never consult that
2050    /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
2051    /// Not lexing it as a keyword closes the whole class rather than the two
2052    /// positions that happened to be noticed.
2053    fn peek_is_by(&self) -> bool {
2054        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
2055    }
2056
2057    /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
2058    /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
2059    /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
2060    fn consume_drop_behaviour(&mut self) {
2061        if matches!(
2062            self.peek(),
2063            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
2064        ) {
2065            self.advance();
2066        }
2067    }
2068
2069    fn expect_ident_like(&mut self) -> Result<String, ParseError> {
2070        let first = match self.advance() {
2071            Token::Ident(s) | Token::QuotedIdent(s) => s,
2072            // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
2073            // per PG's `pg_get_keywords()` classification. SPG tokenizes
2074            // these as named variants for parsing leverage in the
2075            // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
2076            // `BEGIN`, etc.), but they MUST still be usable as table /
2077            // column / alias names in DDL+DML. Sentori migrations like
2078            // 0001_init.sql ship `release TEXT NOT NULL` in the events
2079            // table — the `events.release` column carries the release
2080            // identifier string. Pre-T4 this triggered "expected
2081            // identifier, got Release" and blocked every drop-in user
2082            // whose schema had a column / alias with one of these names.
2083            other if unreserved_keyword_text(&other).is_some() => {
2084                unreserved_keyword_text(&other).unwrap()
2085            }
2086            other => {
2087                return Err(ParseError {
2088                    message: format!("expected identifier, got {other:?}"),
2089                    token_pos: self.consumed_pos(),
2090                });
2091            }
2092        };
2093        // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
2094        // qualify every name with `public.` (and pg_catalog.* for
2095        // functions); SPG is single-schema so we discard the
2096        // prefix and return only the trailing ident. Same shape
2097        // also handles MySQL `db.tbl` cross-database refs (SPG
2098        // ignores the db part).
2099        if matches!(self.peek(), Token::Dot) {
2100            self.advance();
2101            match self.advance() {
2102                Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
2103                other if unreserved_keyword_text(&other).is_some() => {
2104                    return Ok(unreserved_keyword_text(&other).unwrap());
2105                }
2106                other => {
2107                    return Err(ParseError {
2108                        message: format!("expected identifier after '{first}.', got {other:?}"),
2109                        token_pos: self.consumed_pos(),
2110                    });
2111                }
2112            }
2113        }
2114        Ok(first)
2115    }
2116
2117    #[allow(clippy::too_many_lines)]
2118    fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2119        // v7.14.0 — empty / comment-only / semicolon-only input
2120        // (after the lexer strips line + block + MySQL
2121        // conditional comments) lands as Statement::Empty.
2122        // pg_dump and mysqldump emit several wrappers that
2123        // collapse to nothing after stripping (`/*!40101 SET …
2124        // */;`, blank lines between statements); the engine
2125        // returns CommandOk no-op so the dump loads cleanly.
2126        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2127            return Ok(Statement::Empty);
2128        }
2129        // v7.14.0 — pg_dump / mysqldump "noise" statements:
2130        // catalog / metadata DDL that has no behavioural effect
2131        // on SPG's single-schema, single-database, single-user
2132        // model. Consume the whole statement up to the next
2133        // semicolon / EOF and return Empty. This is broader than
2134        // the per-keyword DROP / SET / COMMENT arms but lets the
2135        // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2136        // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2137        // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2138        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2139            let lc = s.to_ascii_lowercase();
2140            // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2141            if lc == "comment" {
2142                return self.parse_comment_on();
2143            }
2144            // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2145            if lc == "grant" || lc == "revoke" {
2146                return self.parse_grant_or_revoke(lc == "grant");
2147            }
2148            // v7.39 (round 277) — the SQL-level prepared-statement
2149            // surface is REAL now. It used to be accepted and dropped
2150            // on the theory that "real execution still happens via the
2151            // extended-query flow" — true only for a driver that uses
2152            // that flow; a plain SQL PREPARE / EXECUTE returned no
2153            // rows at all.
2154            if lc == "prepare" {
2155                return self.parse_prepare();
2156            }
2157            if lc == "execute" {
2158                return self.parse_execute();
2159            }
2160            if lc == "deallocate" {
2161                return self.parse_deallocate();
2162            }
2163            // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2164            // accepted and dropped, so an application's stored-procedure
2165            // invocation reported success and did nothing. SPG has no
2166            // procedure catalog, so every CALL names a procedure that
2167            // does not exist — which is exactly what PG says.
2168            if lc == "call" {
2169                return self.parse_call();
2170            }
2171            // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2172            // names one connection and acts on it.
2173            if lc == "kill" {
2174                return self.parse_kill();
2175            }
2176            if lc == "discard" {
2177                return self.parse_discard();
2178            }
2179            // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2180            // Still performs nothing; the roles are carried out so a name
2181            // that does not exist is refused, as PG18 refuses it.
2182            if lc == "reassign" {
2183                self.advance();
2184                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2185                    self.advance();
2186                }
2187                if self.peek_is_by() {
2188                    self.advance();
2189                }
2190                // Only the roles BEFORE the TO are the ones that must
2191                // exist — `TO` names the new owner, which PG checks as
2192                // well, so both lists are collected.
2193                let mut names = self.take_comma_separated_names();
2194                if matches!(self.peek(), Token::To) {
2195                    self.advance();
2196                    names.extend(self.take_comma_separated_names());
2197                }
2198                self.consume_until_statement_boundary();
2199                return Ok(Statement::ValidateOnly {
2200                    kind: crate::ast::ValidateOnlyKind::RoleName,
2201                    names,
2202                });
2203            }
2204            // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2205            // unconditionally with `no security label providers have been
2206            // loaded`, whatever object it names, because none is loaded.
2207            // SPG has none either; accepting it told the caller a label had
2208            // been applied when nothing anywhere records one.
2209            if lc == "security" {
2210                self.consume_until_statement_boundary();
2211                return Ok(Statement::ValidateOnly {
2212                    kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2213                    names: Vec::new(),
2214                });
2215            }
2216            // v7.39.2 — `USE <db>` is a real statement now, and only in
2217            // the MySQL dialect. It used to be swallowed here with the
2218            // dump noise, so `USE myapp; SELECT DATABASE()` answered the
2219            // same constant it answered before — MySQL 9.7.2 answers
2220            // `myapp`. PostgreSQL has no USE at all, and pg_dump does not
2221            // emit one, but the swallow stays on that side: it was put
2222            // there for restores and taking it away is not this defect.
2223            if lc == "use" {
2224                if self.mysql_dialect {
2225                    self.advance();
2226                    let name = self.expect_ident_like()?;
2227                    return Ok(Statement::UseDatabase(name));
2228                }
2229                self.consume_until_statement_boundary();
2230                return Ok(Statement::Empty);
2231            }
2232            if is_dump_noise_statement(&lc) {
2233                self.consume_until_statement_boundary();
2234                return Ok(Statement::Empty);
2235            }
2236        }
2237        match self.peek() {
2238            Token::Select => self.parse_select_stmt(),
2239            // v7.37.17 (17.6 siblings) — a statement opening with a
2240            // parenthesized query group: `(SELECT … UNION …)
2241            // INTERSECT …`. parse_bare_select's group arm consumes
2242            // the parens; the select parser handles the outer chain
2243            // and tail.
2244            Token::LParen
2245                if matches!(
2246                    self.tokens.get(self.pos + 1),
2247                    Some(Token::Select | Token::LParen | Token::Values)
2248                ) =>
2249            {
2250                self.parse_select_stmt()
2251            }
2252            // v7.37.17 (17.6 siblings) — top-level bare VALUES
2253            // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2254            // Lowers to the same UNION ALL chain the FROM-position
2255            // form uses, then reuses the shared SELECT tail.
2256            Token::Values => {
2257                self.advance(); // VALUES
2258                let mut head = self.parse_values_rows_body()?;
2259                self.parse_select_tail_into(&mut head)?;
2260                Ok(Statement::Select(head))
2261            }
2262            // SQL-standard `TABLE name` shorthand for
2263            // `SELECT * FROM name` — pg_dump never emits it, but
2264            // psql users and PG docs use it constantly. Set-op
2265            // chains and the ORDER BY/LIMIT tail compose like any
2266            // SELECT head.
2267            Token::Table
2268                if matches!(
2269                    self.tokens.get(self.pos + 1),
2270                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2271                ) =>
2272            {
2273                let mut head = self.parse_table_shorthand()?;
2274                self.parse_setop_chain_into(&mut head)?;
2275                self.parse_select_tail_into(&mut head)?;
2276                Ok(Statement::Select(head))
2277            }
2278            // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2279            // body is a dollar-quoted plpgsql block (lexer already
2280            // collapsed `$$…$$` into a single Token::String).
2281            // v7.16.2 — mailrs round-10 A.2: parse the body as a
2282            // real PlPgSqlBlock so the engine can EXECUTE it at
2283            // top level instead of silently swallowing. Pre-
2284            // v7.16.2 the parser threw the body away and the
2285            // engine returned CommandOk for the entire DO; that
2286            // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2287            // $$` into a SEV-1 silent no-op (the IF + the rename
2288            // were both invisible — mailrs's migrate-042 didn't
2289            // actually run). Now the body parses + executes;
2290            // EmbeddedSql inside the block runs immediately
2291            // against the engine (not deferred — we're at top
2292            // level, not inside a trigger row-write loop).
2293            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2294                self.advance();
2295                let body_text = match self.advance() {
2296                    Token::String(s) => s,
2297                    other => {
2298                        return Err(self.err(alloc::format!(
2299                            "expected dollar-quoted body after DO, got {other:?}"
2300                        )));
2301                    }
2302                };
2303                // Optional `LANGUAGE <name>` trailer (idents only).
2304                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2305                    self.advance();
2306                    let _ = self.expect_ident_like()?;
2307                }
2308                // Parse the body — same shape CREATE FUNCTION
2309                // uses for trigger function bodies. If the body
2310                // doesn't parse cleanly we surface the error
2311                // (better than silent no-op).
2312                let block = parse_plpgsql_body(&body_text)?;
2313                Ok(Statement::DoBlock(block))
2314            }
2315            // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2316            // WITH isn't a reserved token in our lexer — comes through
2317            // as `Token::Ident("with")` (case-insensitive).
2318            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2319                self.advance();
2320                self.parse_with_cte_then_select()
2321            }
2322            // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2323            // an identifier — not a reserved keyword.
2324            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2325                self.advance();
2326                let mut analyze = false;
2327                let mut suggest = false;
2328                let mut costs_off = false;
2329                let mut buffers = false;
2330                let mut timing_off = false;
2331                let mut settings = false;
2332                let mut wal = false;
2333                let mut summary_off = false;
2334                let mut format = crate::ast::ExplainFormat::Text;
2335                // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2336                // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2337                // options are comma-separated. Booleans default to ON
2338                // when the value token is omitted (matches PG).
2339                if matches!(self.peek(), Token::LParen) {
2340                    self.advance();
2341                    loop {
2342                        let opt = match self.peek().clone() {
2343                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2344                            other => {
2345                                return Err(self.err(format!(
2346                                    "expected option keyword inside EXPLAIN (…), got {other:?}"
2347                                )));
2348                            }
2349                        };
2350                        self.advance();
2351                        if opt.eq_ignore_ascii_case("suggest") {
2352                            suggest = true;
2353                            // SUGGEST takes no explicit value today.
2354                        } else if opt.eq_ignore_ascii_case("costs") {
2355                            // PG syntax: `COSTS [ON | OFF]`. Default
2356                            // when value omitted is ON, so plain
2357                            // `COSTS` is a no-op. `COSTS OFF` flips.
2358                            // `ON` lexes to `Token::On` (reserved
2359                            // keyword in JOIN ... ON contexts); accept
2360                            // it alongside the bare Ident form so the
2361                            // grammar matches PG verbatim.
2362                            let value = match self.peek().clone() {
2363                                Token::On => {
2364                                    self.advance();
2365                                    true
2366                                }
2367                                Token::Ident(v) | Token::QuotedIdent(v)
2368                                    if v.eq_ignore_ascii_case("off") =>
2369                                {
2370                                    self.advance();
2371                                    false
2372                                }
2373                                Token::Ident(v) | Token::QuotedIdent(v)
2374                                    if v.eq_ignore_ascii_case("true") =>
2375                                {
2376                                    self.advance();
2377                                    true
2378                                }
2379                                _ => true,
2380                            };
2381                            costs_off = !value;
2382                        } else if opt.eq_ignore_ascii_case("analyze")
2383                            || opt.eq_ignore_ascii_case("analyse")
2384                        {
2385                            // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2386                            // Same default-ON rule as ANALYZE keyword form.
2387                            let value = match self.peek().clone() {
2388                                Token::On => {
2389                                    self.advance();
2390                                    true
2391                                }
2392                                Token::Ident(v) | Token::QuotedIdent(v)
2393                                    if v.eq_ignore_ascii_case("off") =>
2394                                {
2395                                    self.advance();
2396                                    false
2397                                }
2398                                Token::Ident(v) | Token::QuotedIdent(v)
2399                                    if v.eq_ignore_ascii_case("true") =>
2400                                {
2401                                    self.advance();
2402                                    true
2403                                }
2404                                _ => true,
2405                            };
2406                            analyze = value;
2407                        } else if opt.eq_ignore_ascii_case("buffers") {
2408                            // v7.37.22 — `BUFFERS [ON|OFF]`.
2409                            let value = match self.peek().clone() {
2410                                Token::On => {
2411                                    self.advance();
2412                                    true
2413                                }
2414                                Token::Ident(v) | Token::QuotedIdent(v)
2415                                    if v.eq_ignore_ascii_case("off") =>
2416                                {
2417                                    self.advance();
2418                                    false
2419                                }
2420                                Token::Ident(v) | Token::QuotedIdent(v)
2421                                    if v.eq_ignore_ascii_case("true") =>
2422                                {
2423                                    self.advance();
2424                                    true
2425                                }
2426                                _ => true,
2427                            };
2428                            buffers = value;
2429                        } else if opt.eq_ignore_ascii_case("timing") {
2430                            // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2431                            // the measured wall-clock annotation.
2432                            let value = match self.peek().clone() {
2433                                Token::On => {
2434                                    self.advance();
2435                                    true
2436                                }
2437                                Token::Ident(v) | Token::QuotedIdent(v)
2438                                    if v.eq_ignore_ascii_case("off") =>
2439                                {
2440                                    self.advance();
2441                                    false
2442                                }
2443                                Token::Ident(v) | Token::QuotedIdent(v)
2444                                    if v.eq_ignore_ascii_case("true") =>
2445                                {
2446                                    self.advance();
2447                                    true
2448                                }
2449                                _ => true,
2450                            };
2451                            timing_off = !value;
2452                        } else if opt.eq_ignore_ascii_case("settings") {
2453                            settings = true;
2454                        } else if opt.eq_ignore_ascii_case("wal") {
2455                            wal = true;
2456                        } else if opt.eq_ignore_ascii_case("summary") {
2457                            // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2458                            // gates the trailing Planning/Execution Time
2459                            // lines now (was accept-and-no-op).
2460                            let value = match self.peek().clone() {
2461                                Token::On => {
2462                                    self.advance();
2463                                    true
2464                                }
2465                                Token::Ident(v) | Token::QuotedIdent(v)
2466                                    if v.eq_ignore_ascii_case("off") =>
2467                                {
2468                                    self.advance();
2469                                    false
2470                                }
2471                                Token::Ident(v) | Token::QuotedIdent(v)
2472                                    if v.eq_ignore_ascii_case("true") =>
2473                                {
2474                                    self.advance();
2475                                    true
2476                                }
2477                                _ => true,
2478                            };
2479                            summary_off = !value;
2480                        } else if opt.eq_ignore_ascii_case("verbose")
2481                            || opt.eq_ignore_ascii_case("format")
2482                        {
2483                            // v7.37.22 — accept-but-no-op the remaining
2484                            // PG options so EXPLAIN-using clients
2485                            // (pgAdmin / DataGrip) don't see syntax
2486                            // errors. FORMAT takes a value (text /
2487                            // json / yaml / xml); skip the next token
2488                            // if it's an ident.
2489                            if opt.eq_ignore_ascii_case("format") {
2490                                if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2491                                {
2492                                    self.advance();
2493                                    format = match v.to_ascii_lowercase().as_str() {
2494                                        "text" => crate::ast::ExplainFormat::Text,
2495                                        "json" => crate::ast::ExplainFormat::Json,
2496                                        "xml" => crate::ast::ExplainFormat::Xml,
2497                                        "yaml" => crate::ast::ExplainFormat::Yaml,
2498                                        other => {
2499                                            return Err(self.err(format!(
2500                                                "EXPLAIN (FORMAT …): unknown format {other:?}; \
2501                                                 supports text, json, xml, yaml"
2502                                            )));
2503                                        }
2504                                    };
2505                                }
2506                            } else {
2507                                // VERBOSE / SUMMARY take optional ON/OFF;
2508                                // consume if present.
2509                                if matches!(self.peek(), Token::On) {
2510                                    self.advance();
2511                                } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2512                                    self.peek().clone()
2513                                    && (v.eq_ignore_ascii_case("off")
2514                                        || v.eq_ignore_ascii_case("true"))
2515                                {
2516                                    self.advance();
2517                                    let _ = v;
2518                                }
2519                            }
2520                        } else {
2521                            return Err(self.err(format!(
2522                                "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2523                            )));
2524                        }
2525                        if matches!(self.peek(), Token::Comma) {
2526                            self.advance();
2527                            continue;
2528                        }
2529                        break;
2530                    }
2531                    if !matches!(self.peek(), Token::RParen) {
2532                        return Err(self.err(format!(
2533                            "expected ')' after EXPLAIN options, got {:?}",
2534                            self.peek()
2535                        )));
2536                    }
2537                    self.advance();
2538                } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2539                    && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2540                {
2541                    self.advance();
2542                    analyze = true;
2543                }
2544                // v7.39 (round 224) — the body may open with WITH (CTEs);
2545                // route through the same CTE-then-SELECT path the top-level
2546                // WITH statement uses. v7.39 (round 225) — DML bodies parse
2547                // too (PG explains INSERT / UPDATE / DELETE).
2548                let inner = match self.peek().clone() {
2549                    Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2550                        self.advance();
2551                        self.parse_with_cte_then_select()?
2552                    }
2553                    Token::Insert => self.parse_insert_stmt(false)?,
2554                    Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2555                        self.advance();
2556                        self.parse_update_after_keyword()?
2557                    }
2558                    Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2559                        self.advance();
2560                        self.parse_delete_after_keyword()?
2561                    }
2562                    _ => self.parse_select_stmt()?,
2563                };
2564                if !matches!(
2565                    inner,
2566                    Statement::Select(_)
2567                        | Statement::Insert(_)
2568                        | Statement::Update(_)
2569                        | Statement::Delete(_)
2570                ) {
2571                    return Err(self.err(format!(
2572                        "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2573                    )));
2574                }
2575                Ok(Statement::Explain(crate::ast::ExplainStatement {
2576                    analyze,
2577                    inner: Box::new(inner),
2578                    suggest,
2579                    costs_off,
2580                    buffers,
2581                    timing_off,
2582                    settings,
2583                    wal,
2584                    summary_off,
2585                    format,
2586                }))
2587            }
2588            Token::Create => self.parse_create_stmt(),
2589            Token::Insert => self.parse_insert_stmt(false),
2590            // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2591            // spelling; route to the same handler. DESC is the
2592            // reserved ORDER BY token, so it gets its own arm.
2593            Token::Ident(s)
2594                if s.eq_ignore_ascii_case("describe")
2595                    && matches!(
2596                        self.tokens.get(self.pos + 1),
2597                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2598                    ) =>
2599            {
2600                self.advance();
2601                let table = self.expect_ident_like()?;
2602                Ok(Statement::ShowColumns(table))
2603            }
2604            Token::Desc
2605                if matches!(
2606                    self.tokens.get(self.pos + 1),
2607                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2608                ) =>
2609            {
2610                self.advance();
2611                let table = self.expect_ident_like()?;
2612                Ok(Statement::ShowColumns(table))
2613            }
2614            // `COPY table [(cols)] TO STDOUT` — the export half of
2615            // pg_dump's COPY pair (the FROM stdin half rides the
2616            // embed import path). Options need a format design and
2617            // error honestly.
2618            Token::Ident(s)
2619                if s.eq_ignore_ascii_case("copy")
2620                    && matches!(
2621                        self.tokens.get(self.pos + 1),
2622                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2623                    ) =>
2624            {
2625                self.advance(); // COPY
2626                let table = self.expect_ident_like()?;
2627                let columns = if matches!(self.peek(), Token::LParen) {
2628                    self.advance();
2629                    let mut cols = alloc::vec![self.expect_ident_like()?];
2630                    while matches!(self.peek(), Token::Comma) {
2631                        self.advance();
2632                        cols.push(self.expect_ident_like()?);
2633                    }
2634                    if !matches!(self.peek(), Token::RParen) {
2635                        return Err(self.err(format!(
2636                            "expected ')' after COPY column list, got {:?}",
2637                            self.peek()
2638                        )));
2639                    }
2640                    self.advance();
2641                    Some(cols)
2642                } else {
2643                    None
2644                };
2645                // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2646                // endpoint. (FROM STDIN still rides the wire/import path —
2647                // its data arrives out of band.)
2648                if matches!(self.peek(), Token::From)
2649                    && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2650                {
2651                    self.advance(); // FROM
2652                    let Token::String(path) = self.advance() else {
2653                        unreachable!()
2654                    };
2655                    let options = self.parse_copy_to_options()?;
2656                    return Ok(Statement::CopyFromFile {
2657                        table,
2658                        columns,
2659                        path,
2660                        options,
2661                    });
2662                }
2663                if !matches!(self.peek(), Token::To) {
2664                    return Err(self.err(format!(
2665                        "COPY: only TO STDOUT is supported here (FROM stdin \
2666                         rides the import path); got {:?}",
2667                        self.peek()
2668                    )));
2669                }
2670                self.advance();
2671                if matches!(self.peek(), Token::String(_)) {
2672                    let Token::String(path) = self.advance() else { unreachable!() };
2673                    let options = self.parse_copy_to_options()?;
2674                    return Ok(Statement::CopyToFile {
2675                        table,
2676                        columns,
2677                        query: None,
2678                        path,
2679                        options,
2680                    });
2681                }
2682                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2683                    return Err(self.err(format!(
2684                        "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2685                        self.peek()
2686                    )));
2687                }
2688                self.advance();
2689                let options = self.parse_copy_to_options()?;
2690                Ok(Statement::CopyTo {
2691                    table,
2692                    columns,
2693                    query: None,
2694                    options,
2695                })
2696            }
2697            // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2698            // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2699            // result set is streamed in COPY format (PG's query form).
2700            Token::Ident(s)
2701                if s.eq_ignore_ascii_case("copy")
2702                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2703            {
2704                self.advance(); // COPY
2705                self.advance(); // (
2706                let query = self.parse_select_stmt()?;
2707                if !matches!(self.peek(), Token::RParen) {
2708                    return Err(self.err(format!(
2709                        "expected ')' after COPY query, got {:?}",
2710                        self.peek()
2711                    )));
2712                }
2713                self.advance(); // )
2714                if !matches!(self.peek(), Token::To) {
2715                    return Err(self.err(format!(
2716                        "COPY (query): only TO STDOUT is supported, got {:?}",
2717                        self.peek()
2718                    )));
2719                }
2720                self.advance();
2721                if matches!(self.peek(), Token::String(_)) {
2722                    let Token::String(path) = self.advance() else { unreachable!() };
2723                    let options = self.parse_copy_to_options()?;
2724                    return Ok(Statement::CopyToFile {
2725                        table: String::new(),
2726                        columns: None,
2727                        query: Some(alloc::boxed::Box::new(query)),
2728                        path,
2729                        options,
2730                    });
2731                }
2732                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2733                    return Err(self.err(format!(
2734                        "COPY (query): TO supports STDOUT only, got {:?}",
2735                        self.peek()
2736                    )));
2737                }
2738                self.advance();
2739                let options = self.parse_copy_to_options()?;
2740                Ok(Statement::CopyTo {
2741                    table: String::new(),
2742                    columns: None,
2743                    query: Some(alloc::boxed::Box::new(query)),
2744                    options,
2745                })
2746            }
2747            // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2748            // Shares the INSERT body; the replace flag lowers it
2749            // onto ON CONFLICT DO UPDATE with an empty assignment
2750            // list (engine: replace the whole row).
2751            Token::Ident(s)
2752                if s.eq_ignore_ascii_case("replace")
2753                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2754            {
2755                self.parse_insert_stmt(true)
2756            }
2757            Token::Begin => {
2758                self.advance();
2759                // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2760                // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2761                // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2762                // is consumed first, then the trailing modes — including the
2763                // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2764                // WORK/TRANSACTION). The explicit level, when present, rides the
2765                // statement so `exec_begin` applies it for this transaction.
2766                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2767                {
2768                    self.advance();
2769                }
2770                let iso = self.parse_isolation_level_clauses()?;
2771                Ok(Statement::Begin(iso))
2772            }
2773            // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2774            // for BEGIN. START is contextual in PG too; pattern-match
2775            // on the ident here. Iso clauses are parse-and-ignored,
2776            // same as BEGIN above.
2777            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2778                self.advance();
2779                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2780                {
2781                    return Err(self.err(alloc::format!(
2782                        "expected TRANSACTION after START, got {:?}",
2783                        self.peek()
2784                    )));
2785                }
2786                self.advance();
2787                let iso = self.parse_isolation_level_clauses()?;
2788                Ok(Statement::Begin(iso))
2789            }
2790            Token::Commit => {
2791                self.advance();
2792                // PG: `COMMIT [WORK | TRANSACTION]`.
2793                if let Token::Ident(w) = self.peek()
2794                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2795                {
2796                    self.advance();
2797                }
2798                Ok(Statement::Commit)
2799            }
2800            // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2801            // COMMIT synonym; pgbench's builtin tpcb-like script closes
2802            // every transaction with `END;` and the drop-in aborted on
2803            // it. Only reachable at statement start (CASE … END lives
2804            // inside expressions), so no ambiguity.
2805            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2806                self.advance();
2807                if let Token::Ident(w) = self.peek()
2808                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2809                {
2810                    self.advance();
2811                }
2812                Ok(Statement::Commit)
2813            }
2814            Token::Rollback => {
2815                self.advance();
2816                // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2817                // savepoint without ending the transaction. Bare
2818                // `ROLLBACK` drops the whole TX.
2819                if matches!(self.peek(), Token::To) {
2820                    self.advance();
2821                    if matches!(self.peek(), Token::Savepoint) {
2822                        self.advance();
2823                    }
2824                    let name = self.expect_ident_like()?;
2825                    Ok(Statement::RollbackToSavepoint(name))
2826                } else {
2827                    Ok(Statement::Rollback)
2828                }
2829            }
2830            Token::Savepoint => {
2831                self.advance();
2832                let name = self.expect_ident_like()?;
2833                Ok(Statement::Savepoint(name))
2834            }
2835            Token::Release => {
2836                self.advance();
2837                // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2838                // is optional in standard SQL.
2839                if matches!(self.peek(), Token::Savepoint) {
2840                    self.advance();
2841                }
2842                let name = self.expect_ident_like()?;
2843                Ok(Statement::ReleaseSavepoint(name))
2844            }
2845            Token::Show => {
2846                self.advance();
2847                // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2848                // v6.1.2 promoted TABLES to a reserved keyword (for
2849                // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2850                // arrives as `Token::Tables` rather than a bare ident.
2851                // USERS / COLUMNS remain bare idents.
2852                let target = match self.advance() {
2853                    Token::Tables => "tables".to_string(),
2854                    // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2855                    // keyword token; recognise it as the SHOW CREATE
2856                    // dispatch keyword too.
2857                    Token::Create => "create".to_string(),
2858                    // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2859                    // keyword too; let SHOW INDEX FROM parse.
2860                    Token::Index => "index".to_string(),
2861                    // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2862                    // reserved (used in aggregate function calls);
2863                    // recognise it here so the parser dispatches
2864                    // to ShowParameter("all") — the engine returns
2865                    // the curated parameter inventory.
2866                    Token::All => "all".to_string(),
2867                    // v7.38.18 (C12) — `SHOW COUNT(*) WARNINGS`, MySQL's
2868                    // spelling for the size of the diagnostics area.
2869                    // MySQL-dialect only: PostgreSQL 18.4 answers this
2870                    // phrase with `syntax error at or near "("`, and a
2871                    // PG session must keep getting exactly that rather
2872                    // than a message about an unknown parameter.
2873                    // `COUNT` arrives as a bare ident; the `(*)` and the
2874                    // trailing keyword are consumed here so the whole
2875                    // form reaches the engine as one parameter name.
2876                    Token::Ident(ref c)
2877                        if self.mysql_dialect
2878                            && c.eq_ignore_ascii_case("count")
2879                            && matches!(self.peek(), Token::LParen) =>
2880                    {
2881                        self.advance();
2882                        if matches!(self.peek(), Token::Star) {
2883                            self.advance();
2884                        }
2885                        if matches!(self.peek(), Token::RParen) {
2886                            self.advance();
2887                        }
2888                        match self.advance() {
2889                            Token::Ident(w) if w.eq_ignore_ascii_case("warnings") => {
2890                                return Ok(Statement::ShowParameter(
2891                                    "count(*) warnings".to_string(),
2892                                ));
2893                            }
2894                            other => {
2895                                return Err(self.err(format!(
2896                                    "expected WARNINGS after SHOW COUNT(*), got {other:?}"
2897                                )));
2898                            }
2899                        }
2900                    }
2901                    Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2902                    other => {
2903                        return Err(self.err(format!(
2904                            "expected SHOW target, got {other:?}"
2905                        )));
2906                    }
2907                };
2908                match target.as_str() {
2909                    "tables" => Ok(Statement::ShowTables),
2910                    "users" => Ok(Statement::ShowUsers),
2911                    // v7.38 轴 4 — `SHOW transaction_isolation`
2912                    // returns the currently-selected isolation level.
2913                    "transaction_isolation" => Ok(Statement::ShowParameter(
2914                        "transaction_isolation".to_string(),
2915                    )),
2916                    // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2917                    // TABLE <t>` returns a 2-column row: (Table,
2918                    // Create Table). mysqldump emits this for every
2919                    // table at scrape time; without it the dump
2920                    // round-trip stalls.
2921                    // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2922                    // FROM <t>` (also spelled `SHOW INDEX` and
2923                    // `SHOW KEYS`). admin / mysqldump probes use
2924                    // it to list per-table indexes.
2925                    "indexes" | "index" | "keys" => {
2926                        if !matches!(self.peek(), Token::From) {
2927                            return Err(self.err(format!(
2928                                "expected FROM after SHOW INDEXES, got {:?}",
2929                                self.peek()
2930                            )));
2931                        }
2932                        self.advance();
2933                        let table = self.expect_ident_like()?;
2934                        Ok(Statement::ShowIndexes(table))
2935                    }
2936                    // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2937                    // `SHOW VARIABLES`. Both return a 2-column row
2938                    // set listing server-side state; clients probe
2939                    // them at connect time.
2940                    "status" => Ok(Statement::ShowStatus),
2941                    "variables" => {
2942                        // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2943                        if matches!(self.peek(), Token::Like) {
2944                            self.advance();
2945                            let pat = match self.advance() {
2946                                Token::String(p) => p,
2947                                other => {
2948                                    return Err(self.err(format!(
2949                                        "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2950                                    )));
2951                                }
2952                            };
2953                            return Ok(Statement::ShowVariablesLike(pat));
2954                        }
2955                        Ok(Statement::ShowVariables)
2956                    }
2957                    // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2958                    "processlist" => Ok(Statement::ShowProcesslist),
2959                    "create" => {
2960                        // SHOW CREATE TABLE / VIEW / DATABASE — only
2961                        // TABLE is supported in v7.17.
2962                        let kind = match self.advance() {
2963                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2964                            Token::Table => "table".to_string(),
2965                            other => {
2966                                return Err(self.err(format!(
2967                                    "expected TABLE after SHOW CREATE, got {other:?}"
2968                                )));
2969                            }
2970                        };
2971                        if !kind.eq_ignore_ascii_case("table") {
2972                            return Err(self.err(format!(
2973                                "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2974                            )));
2975                        }
2976                        let name = self.expect_ident_like()?;
2977                        Ok(Statement::ShowCreateTable(name))
2978                    }
2979                    // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2980                    // (and `SHOW SCHEMAS` alias). The mysql client uses
2981                    // it to populate the database selector at connect
2982                    // time; without it `mysql -p` errors before the
2983                    // first user query.
2984                    "databases" | "schemas" => Ok(Statement::ShowDatabases),
2985                    // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2986                    // keyword on its own; it lands here as a bare
2987                    // ident. Returning all publications + their
2988                    // scope summary.
2989                    "publications" => Ok(Statement::ShowPublications),
2990                    // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2991                    "subscriptions" => Ok(Statement::ShowSubscriptions),
2992                    "columns" => {
2993                        if !matches!(self.peek(), Token::From) {
2994                            return Err(self.err(format!(
2995                                "expected FROM after SHOW COLUMNS, got {:?}",
2996                                self.peek()
2997                            )));
2998                        }
2999                        self.advance();
3000                        let table = self.expect_ident_like()?;
3001                        Ok(Statement::ShowColumns(table))
3002                    }
3003                    // v7.38 轴 4 surface — `SHOW <param>` for any
3004                    // remaining session / preset parameter name
3005                    // (server_version, search_path, client_encoding,
3006                    // …). The engine's ShowParameter handler does the
3007                    // dispatch; unrecognised names error there with
3008                    // a pointer to pg_settings, not at parse time —
3009                    // so a driver that issues `SHOW spam_setting`
3010                    // gets a clear runtime error instead of a
3011                    // confusing "unknown SHOW target".
3012                    other => {
3013                        // v7.38 (read01 P3.20) — a custom namespaced GUC
3014                        // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
3015                        // consume the dotted tail so it round-trips with
3016                        // `SET app.foo` / `current_setting('app.foo')`.
3017                        let mut full = other.to_string();
3018                        while matches!(self.peek(), Token::Dot) {
3019                            self.advance();
3020                            let seg = self.expect_ident_like()?;
3021                            full.push('.');
3022                            full.push_str(&seg.to_ascii_lowercase());
3023                        }
3024                        Ok(Statement::ShowParameter(full))
3025                    }
3026                }
3027            }
3028            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
3029            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
3030            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
3031            // arrived as a bare ident; tokenising it dedicatedly
3032            // keeps the dispatch tree small.
3033            Token::Drop => {
3034                self.advance();
3035                match self.peek() {
3036                    // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
3037                    // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
3038                    // around DROP ROLE cleanup. SPG has no role-owner
3039                    // model, so consume to boundary as a no-op.
3040                    Token::Ident(s) | Token::QuotedIdent(s)
3041                        if s.eq_ignore_ascii_case("owned") =>
3042                    {
3043                        // v7.39 (round 696) — still a no-op (SPG has no
3044                        // role-owner model), but the ROLE is carried out so
3045                        // the engine can refuse one that does not exist,
3046                        // which is what PG18 does.
3047                        self.advance();
3048                        if self.peek_is_by() {
3049                            self.advance();
3050                        }
3051                        let names = self.take_comma_separated_names();
3052                        self.consume_until_statement_boundary();
3053                        Ok(Statement::ValidateOnly {
3054                            kind: crate::ast::ValidateOnlyKind::RoleName,
3055                            names,
3056                        })
3057                    }
3058                    // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
3059                    // It drops only a TEMPORARY table, and name resolution
3060                    // already prefers the session's own, so the keyword is
3061                    // consumed and the ordinary DROP TABLE path runs.
3062                    Token::Ident(s) | Token::QuotedIdent(s)
3063                        if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
3064                    {
3065                        self.advance();
3066                        if !matches!(self.peek(), Token::Table) {
3067                            return Err(self.err(alloc::format!(
3068                                "expected TABLE after DROP TEMPORARY, got {:?}",
3069                                self.peek()
3070                            )));
3071                        }
3072                        self.parse_drop_table_after_keyword()
3073                    }
3074                    Token::Publication => {
3075                        self.advance();
3076                        // v7.39 (round 754, F31-B4) — the round-753
3077                        // audit probe tripped over the missing
3078                        // `IF EXISTS` here (syntax error).
3079                        let if_exists = self.consume_if_exists();
3080                        let name = self.expect_ident_or_string()?;
3081                        Ok(Statement::DropPublication { name, if_exists })
3082                    }
3083                    Token::Subscription => {
3084                        self.advance();
3085                        let if_exists = self.consume_if_exists();
3086                        let name = self.expect_ident_or_string()?;
3087                        Ok(Statement::DropSubscription { name, if_exists })
3088                    }
3089                    Token::Ident(s) | Token::QuotedIdent(s)
3090                        if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3091                    {
3092                        self.advance();
3093                        // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3094                        // login user IS a role in PG, and SPG's store holds
3095                        // both. `IF EXISTS` is accepted on either spelling.
3096                        let if_exists = self.consume_if_exists();
3097                        let name = self.expect_ident_or_string()?;
3098                        Ok(Statement::DropUser { name, if_exists })
3099                    }
3100                    // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3101                    // CREATE DATABASE has parsed since v7.14 and this did
3102                    // not, so `DROP DATABASE IF EXISTS x` — what every
3103                    // teardown script and pg_dumpall preamble opens with —
3104                    // came back as a syntax error, which IF EXISTS cannot
3105                    // soften. The name is carried so the engine can answer
3106                    // the way PG does; PG never lets this succeed on a
3107                    // single-database server, since the name is either
3108                    // unknown ("database … does not exist", or a notice
3109                    // under IF EXISTS) or the one you are connected to
3110                    // ("cannot drop the currently open database").
3111                    Token::Ident(s) | Token::QuotedIdent(s)
3112                        if s.eq_ignore_ascii_case("database") =>
3113                    {
3114                        self.advance();
3115                        let if_exists = self.consume_if_exists();
3116                        let name = self.expect_ident_or_string()?;
3117                        self.consume_until_statement_boundary();
3118                        Ok(Statement::DropDatabase { name, if_exists })
3119                    }
3120                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3121                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3122                        self.advance();
3123                        let if_exists = self.consume_if_exists();
3124                        let name = self.expect_ident_like()?;
3125                        // ON <table>
3126                        if !matches!(self.peek(), Token::On) {
3127                            return Err(self.err(alloc::format!(
3128                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3129                                self.peek()
3130                            )));
3131                        }
3132                        self.advance();
3133                        let table = self.expect_ident_like()?;
3134                        Ok(Statement::DropTrigger {
3135                            name,
3136                            table,
3137                            if_exists,
3138                        })
3139                    }
3140                    // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3141                    // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3142                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3143                        self.advance();
3144                        let if_exists = self.consume_if_exists();
3145                        let name = self.expect_ident_like()?;
3146                        if !matches!(self.peek(), Token::On) {
3147                            return Err(self.err(alloc::format!(
3148                                "expected ON <table> after DROP RULE {name:?}, got {:?}",
3149                                self.peek()
3150                            )));
3151                        }
3152                        self.advance();
3153                        let table = self.expect_ident_like()?;
3154                        // Optional CASCADE / RESTRICT — accepted, no effect.
3155                        self.consume_until_statement_boundary();
3156                        Ok(Statement::DropRule {
3157                            name,
3158                            table,
3159                            if_exists,
3160                        })
3161                    }
3162                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3163                    // v7.12.4 ignores any optional arg-list (signature-
3164                    // based overload disambiguation lands in v7.12.5+).
3165                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3166                        self.advance();
3167                        let if_exists = self.consume_if_exists();
3168                        let name = self.expect_ident_like()?;
3169                        // v7.39 (read01 round 62) — the argument list identifies
3170                        // WHICH overload to drop, so it is captured, not
3171                        // discarded. `DROP FUNCTION f` (no list) is legal when
3172                        // the name is unambiguous; the engine enforces that.
3173                        let args = if matches!(self.peek(), Token::LParen) {
3174                            Some(self.parse_function_signature_types()?)
3175                        } else {
3176                            None
3177                        };
3178                        // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3179                        // trailer, which `DROP TABLE` and `DROP INDEX` have
3180                        // accepted since v7.14 and this one refused outright.
3181                        // pg_dump writes it, so refusing was a parse error in
3182                        // the middle of a restore. SPG drops the function
3183                        // either way — it tracks no dependents to cascade to —
3184                        // which is the same reading the other two give it.
3185                        self.consume_drop_behaviour();
3186                        Ok(Statement::DropFunction {
3187                            name,
3188                            args,
3189                            if_exists,
3190                        })
3191                    }
3192                    // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3193                    // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3194                    // emit DROP TABLE IF EXISTS at the head of every
3195                    // CREATE TABLE block so re-importing a dump
3196                    // overwrites prior state. SPG accepts and removes
3197                    // matching tables; CASCADE/RESTRICT trailers
3198                    // accepted silently.
3199                    Token::Table => self.parse_drop_table_after_keyword(),
3200                    // v7.14.0 — DROP INDEX [IF EXISTS] name
3201                    // [CASCADE|RESTRICT]. PG / mysqldump emit this
3202                    // for partial-index renames and pgvector
3203                    // migrations. SPG removes the matching index;
3204                    // IF EXISTS makes the drop idempotent.
3205                    Token::Index => {
3206                        self.advance();
3207                        let if_exists = self.consume_if_exists();
3208                        let name = self.expect_ident_like()?;
3209                        if matches!(
3210                            self.peek(),
3211                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3212                                || s.eq_ignore_ascii_case("restrict")
3213                        ) {
3214                            self.advance();
3215                        }
3216                        Ok(Statement::DropIndex { name, if_exists })
3217                    }
3218                    // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3219                    // [CASCADE|RESTRICT]. SPG is single-database;
3220                    // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3221                    // name [, name…] [CASCADE | RESTRICT]. Real
3222                    // unregister (was silent no-op pre-v7.17).
3223                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3224                        self.advance();
3225                        let if_exists = self.consume_if_exists();
3226                        let mut names = vec![self.expect_ident_like()?];
3227                        while matches!(self.peek(), Token::Comma) {
3228                            self.advance();
3229                            names.push(self.expect_ident_like()?);
3230                        }
3231                        if matches!(
3232                            self.peek(),
3233                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3234                                || s.eq_ignore_ascii_case("restrict")
3235                        ) {
3236                            self.advance();
3237                        }
3238                        Ok(Statement::DropSchema { names, if_exists })
3239                    }
3240                    // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3241                    // name [, name…] [CASCADE|RESTRICT].
3242                    Token::Ident(s) | Token::QuotedIdent(s)
3243                        if s.eq_ignore_ascii_case("type") =>
3244                    {
3245                        self.advance();
3246                        let if_exists = self.consume_if_exists();
3247                        let mut names = vec![self.expect_ident_like()?];
3248                        while matches!(self.peek(), Token::Comma) {
3249                            self.advance();
3250                            names.push(self.expect_ident_like()?);
3251                        }
3252                        if matches!(
3253                            self.peek(),
3254                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3255                                || s.eq_ignore_ascii_case("restrict")
3256                        ) {
3257                            self.advance();
3258                        }
3259                        Ok(Statement::DropType { names, if_exists })
3260                    }
3261                    // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3262                    // name [, name…] [CASCADE|RESTRICT].
3263                    Token::Ident(s) | Token::QuotedIdent(s)
3264                        if s.eq_ignore_ascii_case("domain") =>
3265                    {
3266                        self.advance();
3267                        let if_exists = self.consume_if_exists();
3268                        let mut names = vec![self.expect_ident_like()?];
3269                        while matches!(self.peek(), Token::Comma) {
3270                            self.advance();
3271                            names.push(self.expect_ident_like()?);
3272                        }
3273                        if matches!(
3274                            self.peek(),
3275                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3276                                || s.eq_ignore_ascii_case("restrict")
3277                        ) {
3278                            self.advance();
3279                        }
3280                        Ok(Statement::DropDomain { names, if_exists })
3281                    }
3282                    // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3283                    // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3284                    Token::Ident(s) | Token::QuotedIdent(s)
3285                        if s.eq_ignore_ascii_case("materialized") =>
3286                    {
3287                        self.advance();
3288                        let nxt = self.peek().clone();
3289                        if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3290                        {
3291                            return Err(self.err(alloc::format!(
3292                                "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3293                            )));
3294                        }
3295                        self.advance();
3296                        let if_exists = self.consume_if_exists();
3297                        let mut names = vec![self.expect_ident_like()?];
3298                        while matches!(self.peek(), Token::Comma) {
3299                            self.advance();
3300                            names.push(self.expect_ident_like()?);
3301                        }
3302                        if matches!(
3303                            self.peek(),
3304                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3305                                || s.eq_ignore_ascii_case("restrict")
3306                        ) {
3307                            self.advance();
3308                        }
3309                        Ok(Statement::DropMaterializedView { names, if_exists })
3310                    }
3311                    // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3312                    // name [, name…] [CASCADE|RESTRICT].
3313                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
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::DropView { names, if_exists })
3329                    }
3330                    // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3331                    // [CASCADE|RESTRICT]. Real removal from catalog
3332                    // (was a silent no-op pre-v7.17).
3333                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3334                        self.advance();
3335                        let if_exists = self.consume_if_exists();
3336                        let mut names = vec![self.expect_ident_like()?];
3337                        while matches!(self.peek(), Token::Comma) {
3338                            self.advance();
3339                            names.push(self.expect_ident_like()?);
3340                        }
3341                        if matches!(
3342                            self.peek(),
3343                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3344                                || s.eq_ignore_ascii_case("restrict")
3345                        ) {
3346                            self.advance();
3347                        }
3348                        Ok(Statement::DropSequence { names, if_exists })
3349                    }
3350                    // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3351                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3352                        self.advance();
3353                        self.parse_drop_policy_after_keyword()
3354                    }
3355                    // v7.37.17 (17.6 siblings) — DROP <target> for
3356                    // targets SPG doesn't natively track. pg_dump
3357                    // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3358                    // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3359                    // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3360                    // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3361                    // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3362                    // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3363                    // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3364                    // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3365                    // etc. — accept + Empty-return so pg_dump tails
3366                    // load through. Materialized-view drop dispatches
3367                    // to the existing DropTable path when the token
3368                    // is Materialized-View-shaped (elsewhere in
3369                    // this parser).
3370                    Token::Ident(s) | Token::QuotedIdent(s)
3371                        if s.eq_ignore_ascii_case("text")
3372                            // The DROP dispatch matches on PEEK — `text` is
3373                            // not yet consumed, so SEARCH/CONFIGURATION sit
3374                            // at pos+1/pos+2 (the round-695 trap's mirror).
3375                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3376                            && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3377                    {
3378                        // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3379                        // validates the name; DICTIONARY / PARSER / TEMPLATE
3380                        // stay in the noise arm below.
3381                        self.advance(); // TEXT
3382                        self.advance(); // SEARCH
3383                        self.advance(); // CONFIGURATION
3384                        let if_exists = self.consume_if_exists();
3385                        let names = self.take_comma_separated_names();
3386                        self.consume_until_statement_boundary();
3387                        if if_exists {
3388                            return Ok(Statement::Empty);
3389                        }
3390                        Ok(Statement::ValidateOnly {
3391                            kind: crate::ast::ValidateOnlyKind::TsConfigName,
3392                            names,
3393                        })
3394                    }
3395                    Token::Ident(s) | Token::QuotedIdent(s)
3396                        if matches!(
3397                            s.to_ascii_lowercase().as_str(),
3398                            "type"
3399                                | "domain"
3400                                | "operator"
3401                                | "cast"
3402                                // `text` = TEXT SEARCH DICTIONARY / PARSER /
3403                                // TEMPLATE (CONFIGURATION intercepted above).
3404                                | "text"
3405                                | "materialized"
3406                                | "large"
3407                                | "role"
3408                                | "access"
3409                                | "procedure"
3410                                | "routine"
3411                        ) =>
3412                    {
3413                        self.consume_until_statement_boundary();
3414                        Ok(Statement::Empty)
3415                    }
3416                    // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3417                    // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3418                    // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3419                    // foreign-data warning family (round 706) so a
3420                    // CREATE→DROP sequence in a dump stays consistent.
3421                    Token::Ident(s) | Token::QuotedIdent(s)
3422                        if s.eq_ignore_ascii_case("server")
3423                            || s.eq_ignore_ascii_case("foreign") =>
3424                    {
3425                        self.advance();
3426                        self.consume_until_statement_boundary();
3427                        Ok(Statement::ValidateOnly {
3428                            kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3429                            names: Vec::new(),
3430                        })
3431                    }
3432                    Token::Ident(s) | Token::QuotedIdent(s)
3433                        if s.eq_ignore_ascii_case("collation")
3434                            || s.eq_ignore_ascii_case("tablespace") =>
3435                    {
3436                        let kind = if s.eq_ignore_ascii_case("collation") {
3437                            crate::ast::ValidateOnlyKind::CollationName
3438                        } else {
3439                            crate::ast::ValidateOnlyKind::TablespaceName
3440                        };
3441                        self.advance();
3442                        let if_exists = self.consume_if_exists();
3443                        let names = self.take_comma_separated_names();
3444                        self.consume_until_statement_boundary();
3445                        if if_exists {
3446                            return Ok(Statement::Empty);
3447                        }
3448                        Ok(Statement::ValidateOnly { kind, names })
3449                    }
3450                    Token::Ident(s) | Token::QuotedIdent(s)
3451                        if s.eq_ignore_ascii_case("event") =>
3452                    {
3453                        self.advance();
3454                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3455                        {
3456                            self.advance();
3457                        }
3458                        let if_exists = self.consume_if_exists();
3459                        let names = self.take_comma_separated_names();
3460                        self.consume_until_statement_boundary();
3461                        if if_exists {
3462                            return Ok(Statement::Empty);
3463                        }
3464                        Ok(Statement::ValidateOnly {
3465                            kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3466                            names,
3467                        })
3468                    }
3469                    // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3470                    // leave the noise list; see the ValidateOnly kinds.
3471                    Token::Ident(s) | Token::QuotedIdent(s)
3472                        if s.eq_ignore_ascii_case("conversion")
3473                            || s.eq_ignore_ascii_case("language")
3474                            // `DROP PROCEDURAL LANGUAGE` puts the modifier
3475                            // FIRST — the first draft looked for it after.
3476                            || s.eq_ignore_ascii_case("procedural") =>
3477                    {
3478                        let kind = if s.eq_ignore_ascii_case("conversion") {
3479                            crate::ast::ValidateOnlyKind::ConversionName
3480                        } else {
3481                            crate::ast::ValidateOnlyKind::LanguageName
3482                        };
3483                        self.advance();
3484                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3485                        {
3486                            self.advance();
3487                        }
3488                        let if_exists = self.consume_if_exists();
3489                        let names = self.take_comma_separated_names();
3490                        self.consume_until_statement_boundary();
3491                        if if_exists {
3492                            return Ok(Statement::Empty);
3493                        }
3494                        Ok(Statement::ValidateOnly { kind, names })
3495                    }
3496                    // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3497                    // name(argtypes)[, …]`. Parsed for real so the engine
3498                    // can answer as PG does; see Statement::DropAggregate.
3499                    Token::Ident(s) | Token::QuotedIdent(s)
3500                        if s.eq_ignore_ascii_case("aggregate") =>
3501                    {
3502                        self.advance();
3503                        let if_exists = self.consume_if_exists();
3504                        let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3505                        loop {
3506                            let name = self.expect_ident_like()?;
3507                            if !matches!(self.peek(), Token::LParen) {
3508                                return Err(self.err(alloc::format!(
3509                                    "expected argument list after DROP AGGREGATE {name}"
3510                                )));
3511                            }
3512                            self.advance();
3513                            let mut args: Vec<String> = Vec::new();
3514                            let mut star = false;
3515                            loop {
3516                                match self.peek().clone() {
3517                                    Token::RParen => {
3518                                        self.advance();
3519                                        break;
3520                                    }
3521                                    Token::Star => {
3522                                        self.advance();
3523                                        star = true;
3524                                    }
3525                                    Token::Comma => {
3526                                        self.advance();
3527                                    }
3528                                    _ => {
3529                                        // A type name may be multi-token
3530                                        // (`double precision`); glue idents
3531                                        // until , or ).
3532                                        let mut t = self.expect_ident_like()?;
3533                                        while let Token::Ident(nx) = self.peek() {
3534                                            let nx = nx.clone();
3535                                            self.advance();
3536                                            t.push(' ');
3537                                            t.push_str(&nx);
3538                                        }
3539                                        args.push(t);
3540                                    }
3541                                }
3542                            }
3543                            items.push((name, if star { None } else { Some(args) }));
3544                            if matches!(self.peek(), Token::Comma) {
3545                                self.advance();
3546                            } else {
3547                                break;
3548                            }
3549                        }
3550                        self.consume_until_statement_boundary();
3551                        Ok(Statement::DropAggregate { if_exists, items })
3552                    }
3553                    // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3554                    // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3555                    // installed; `IF EXISTS` is the spelling that says do
3556                    // not, and it keeps the no-op.
3557                    Token::Ident(s) | Token::QuotedIdent(s)
3558                        if s.eq_ignore_ascii_case("extension") =>
3559                    {
3560                        self.advance();
3561                        let if_exists = self.consume_if_exists();
3562                        let names = self.take_comma_separated_names();
3563                        self.consume_until_statement_boundary();
3564                        if if_exists {
3565                            return Ok(Statement::Empty);
3566                        }
3567                        Ok(Statement::ValidateOnly {
3568                            kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3569                            names,
3570                        })
3571                    }
3572                    Token::Ident(s) | Token::QuotedIdent(s)
3573                        if s.eq_ignore_ascii_case("statistics") =>
3574                    {
3575                        self.parse_drop_statistics_after_drop()
3576                    }
3577                    other => Err(self.err(format!(
3578                        "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3579                         SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3580                    ))),
3581                }
3582            }
3583            // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3584            // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3585            // and accepted before the view name. SPG materialised
3586            // views re-evaluate on read (always-fresh semantics), so
3587            // the CONCURRENTLY-vs-serial distinction has no runtime
3588            // effect — the refresh body does not block readers either
3589            // way. Same accept-and-no-op pattern as DETACH PARTITION
3590            // CONCURRENTLY (16.5).
3591            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3592                self.advance();
3593                let nxt = self.peek().clone();
3594                if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3595                {
3596                    return Err(self.err(alloc::format!(
3597                        "expected MATERIALIZED after REFRESH, got {nxt:?}"
3598                    )));
3599                }
3600                self.advance();
3601                let nxt2 = self.peek().clone();
3602                if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3603                {
3604                    return Err(self.err(alloc::format!(
3605                        "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3606                    )));
3607                }
3608                self.advance();
3609                // Optional CONCURRENTLY noise word — consumed without
3610                // changing semantics.
3611                if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3612                {
3613                    self.advance();
3614                }
3615                let name = self.expect_ident_like()?;
3616                let with_data = self.parse_optional_with_data(true)?;
3617                Ok(Statement::RefreshMaterializedView { name, with_data })
3618            }
3619            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3620                self.advance();
3621                self.parse_update_after_keyword()
3622            }
3623            // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3624            // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3625            // [CASCADE | RESTRICT]. Clears every row from each named
3626            // table. Parses at the top level; the engine dispatcher
3627            // walks Statement::Truncate.
3628            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3629                self.advance();
3630                // Optional TABLE noise word — PG accepts both the reserved
3631                // token and the bare identifier spelling.
3632                if matches!(self.peek(), Token::Table)
3633                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3634                {
3635                    self.advance();
3636                }
3637                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3638                // not absorbed. The lookahead keeps a table genuinely
3639                // called `only` working: the keyword is a keyword only
3640                // when a name follows it.
3641                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3642                    if s.eq_ignore_ascii_case("only"))
3643                    && matches!(
3644                        self.tokens.get(self.pos + 1),
3645                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3646                    );
3647                if only {
3648                    self.advance();
3649                }
3650                // Table names (comma-separated).
3651                let mut tables = Vec::new();
3652                loop {
3653                    tables.push(self.expect_ident_like()?);
3654                    if matches!(self.peek(), Token::Comma) {
3655                        self.advance();
3656                        continue;
3657                    }
3658                    break;
3659                }
3660                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3661                let mut restart_identity = false;
3662                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3663                {
3664                    self.advance();
3665                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3666                    {
3667                        self.advance();
3668                        restart_identity = true;
3669                    }
3670                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3671                {
3672                    self.advance();
3673                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3674                    {
3675                        self.advance();
3676                    }
3677                }
3678                // Optional CASCADE / RESTRICT.
3679                let mut cascade = false;
3680                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3681                {
3682                    self.advance();
3683                    cascade = true;
3684                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3685                {
3686                    self.advance();
3687                }
3688                Ok(Statement::Truncate {
3689                    tables,
3690                    restart_identity,
3691                    cascade,
3692                    only,
3693                })
3694            }
3695            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3696            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3697            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3698            // rows change so the index tree is always up-to-date;
3699            // REINDEX is a strict no-op. Accept the whole statement
3700            // shape to boundary for pg_dump round-trip compatibility.
3701            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3702                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3703                // index bloat to rebuild, so the work stays a no-op, but PG
3704                // validates what it was pointed at and this swallowed the
3705                // name at parse time — `REINDEX TABLE typo` reported
3706                // success. Measured on PG18: INDEX / TABLE name a relation,
3707                // SCHEMA a schema, SYSTEM nothing.
3708                self.advance();
3709                self.parse_reindex_tail()
3710            }
3711            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3712            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3713            // SPG has no MVCC bloat today (Phase D visibility map
3714            // queues with v7.38); the freezer collapses hot-tier
3715            // rows into cold segments automatically. VACUUM is a
3716            // no-op — pg_dump maintenance scripts and Discourse's
3717            // periodic-maintenance path both emit it.
3718            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3719            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3720            // actual bloat, so the pre-MVCC accept-and-ignore posture
3721            // became a silent no-op on a customer's manual reclaim.
3722            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3723            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3724            // ANALYZE is captured, the optional table name is captured.
3725            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3726                self.advance();
3727                // Parenthesised option list: absorb it.
3728                if matches!(self.peek(), Token::LParen) {
3729                    let mut depth = 0usize;
3730                    loop {
3731                        match self.advance() {
3732                            Token::LParen => depth += 1,
3733                            Token::RParen => {
3734                                depth -= 1;
3735                                if depth == 0 {
3736                                    break;
3737                                }
3738                            }
3739                            Token::Eof => break,
3740                            _ => {}
3741                        }
3742                    }
3743                }
3744                let mut analyze = false;
3745                let mut table: Option<String> = None;
3746                loop {
3747                    match self.peek() {
3748                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3749                        // an identifier, so the loop below broke out on it and
3750                        // dropped the table name: `VACUUM FULL nosuch` was
3751                        // accepted where `VACUUM nosuch` was refused.
3752                        Token::Full => {
3753                            self.advance();
3754                        }
3755                        Token::Ident(w) | Token::QuotedIdent(w) => {
3756                            let wl = w.to_ascii_lowercase();
3757                            match wl.as_str() {
3758                                "full" | "freeze" | "verbose" => {
3759                                    self.advance();
3760                                }
3761                                "analyze" | "analyse" => {
3762                                    analyze = true;
3763                                    self.advance();
3764                                }
3765                                _ => {
3766                                    table = Some(self.expect_ident_like()?);
3767                                    break;
3768                                }
3769                            }
3770                        }
3771                        _ => break,
3772                    }
3773                }
3774                // Optional trailing column list / anything else to the
3775                // statement boundary (PG accepts per-column ANALYZE).
3776                self.consume_until_statement_boundary();
3777                Ok(Statement::Vacuum { table, analyze })
3778            }
3779            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3780            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3781            // <index>. PG stores rows in physical order matching
3782            // an index; SPG's hot-tier is append-only + cold-tier
3783            // is segment-frozen, so clustering has no persistent
3784            // effect. Accept-and-no-op for pg_dump compat.
3785            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3786                // v7.39 (round 535) — same as REINDEX above: the relation is
3787                // carried so the engine can refuse one that does not exist.
3788                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3789                self.advance();
3790                self.parse_cluster_tail()
3791            }
3792            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3793            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3794            // optional string payload; UNLISTEN takes a channel or `*`.
3795            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3796                self.advance();
3797                let ch = match self.advance() {
3798                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3799                    other => {
3800                        return Err(self.err(format!(
3801                            "expected channel name after LISTEN, got {other:?}"
3802                        )));
3803                    }
3804                };
3805                Ok(Statement::Listen(ch))
3806            }
3807            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3808                self.advance();
3809                let channel = match self.advance() {
3810                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3811                    other => {
3812                        return Err(self.err(format!(
3813                            "expected channel name after NOTIFY, got {other:?}"
3814                        )));
3815                    }
3816                };
3817                let payload = if matches!(self.peek(), Token::Comma) {
3818                    self.advance();
3819                    match self.advance() {
3820                        Token::String(p) => Some(p),
3821                        other => {
3822                            return Err(self.err(format!(
3823                                "expected string payload after NOTIFY <channel>, got {other:?}"
3824                            )));
3825                        }
3826                    }
3827                } else {
3828                    None
3829                };
3830                Ok(Statement::Notify { channel, payload })
3831            }
3832            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3833                self.advance();
3834                match self.advance() {
3835                    Token::Star => Ok(Statement::Unlisten(None)),
3836                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3837                    other => Err(self.err(format!(
3838                        "expected channel name or * after UNLISTEN, got {other:?}"
3839                    ))),
3840                }
3841            }
3842            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3843            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3844            // process-wide write lock today; explicit LOCK has no
3845            // effect. Accept-and-no-op for pg_dump / migration
3846            // compat.
3847            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3848                self.advance();
3849                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3850                // engine holds a process-wide write lock), but the TABLE
3851                // NAME is now carried out so the engine can refuse one that
3852                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3853                // READ|WRITE` is a different statement with the same first
3854                // word; it keeps the old no-op, because a MySQL dump's
3855                // bracket names tables it is about to create.
3856                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3857                    if k.eq_ignore_ascii_case("tables"));
3858                if mysql_tables {
3859                    self.consume_until_statement_boundary();
3860                    return Ok(Statement::Empty);
3861                }
3862                if matches!(self.peek(), Token::Table) {
3863                    self.advance();
3864                }
3865                let names = self.take_comma_separated_names();
3866                self.consume_until_statement_boundary();
3867                Ok(Statement::ValidateOnly {
3868                    kind: crate::ast::ValidateOnlyKind::LockTable,
3869                    names,
3870                })
3871            }
3872            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3873            // durability marker + snapshot in PG. SPG has WAL
3874            // checkpointing on a byte / time schedule (v7.37.10
3875            // 60s / 4 MiB defaults). The bare statement parses to
3876            // `Statement::Empty` here (the no_std engine owns no
3877            // WAL / snapshot); v7.37 Epic Du wires the HOST
3878            // (embedded `Database::execute_buffered`, via
3879            // `sql_is_checkpoint`) to force an immediate synchronous
3880            // checkpoint through `Database::checkpoint` — a real
3881            // durability barrier, matching PG.
3882            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3883                self.advance();
3884                self.consume_until_statement_boundary();
3885                Ok(Statement::Empty)
3886            }
3887            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3888                self.advance();
3889                self.parse_delete_after_keyword()
3890            }
3891            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3892            // ALTER is not a reserved keyword in the lexer — handled
3893            // as a bare ident here.
3894            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3895                self.advance();
3896                self.parse_alter_after_keyword()
3897            }
3898            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3899            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3900            // additions needed.
3901            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3902                self.advance();
3903                self.parse_wait_after_keyword()
3904            }
3905            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3906            // Bare ANALYZE → analyse every user table; ANALYZE
3907            // <name> → re-stats one. The argument is an optional
3908            // ident (or quoted ident); anything else is a parse
3909            // error.
3910            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3911            // `WHERE` filter (carved out per V6_7_DESIGN.md
3912            // STABILITY). Lex order: identifier "compact" → "cold"
3913            // → "segments". Anything else after `COMPACT` is a
3914            // parse error.
3915            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3916                self.advance();
3917                let next = self.peek().clone();
3918                let cold = match next {
3919                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3920                    _ => {
3921                        return Err(
3922                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3923                        );
3924                    }
3925                };
3926                if !cold.eq_ignore_ascii_case("cold") {
3927                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3928                }
3929                self.advance();
3930                let next = self.peek().clone();
3931                let segments = match next {
3932                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3933                    _ => {
3934                        return Err(self.err(format!(
3935                            "expected SEGMENTS after COMPACT COLD, got {:?}",
3936                            self.peek()
3937                        )));
3938                    }
3939                };
3940                if !segments.eq_ignore_ascii_case("segments") {
3941                    return Err(self.err(format!(
3942                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3943                    )));
3944                }
3945                self.advance();
3946                Ok(Statement::CompactColdSegments)
3947            }
3948            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3949            // Parsed as a case-insensitive identifier since MERGE
3950            // isn't a reserved lexer keyword (collides with the
3951            // mysqldump `ALGORITHM = MERGE` view clause if it
3952            // were); the inner parser drives the rest of the
3953            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3954            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3955                self.advance();
3956                self.parse_merge_after_keyword()
3957            }
3958            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3959                self.advance();
3960                let target = match self.peek() {
3961                    Token::Eof | Token::Semicolon => None,
3962                    Token::Ident(_) | Token::QuotedIdent(_) => {
3963                        Some(self.expect_ident_like()?)
3964                    }
3965                    other => {
3966                        return Err(self.err(format!(
3967                            "expected table name or end of statement after ANALYZE, got {other:?}"
3968                        )));
3969                    }
3970                };
3971                // v7.39 (round 776, F31 J7) — the per-column form
3972                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3973                // here while the VACUUM arm already consumed it; SPG
3974                // analyzes whole tables, so the list parses and is
3975                // accepted like the VACUUM path's.
3976                if target.is_some() && matches!(self.peek(), Token::LParen) {
3977                    self.advance();
3978                    loop {
3979                        let _ = self.expect_ident_like()?;
3980                        match self.peek() {
3981                            Token::Comma => {
3982                                self.advance();
3983                            }
3984                            Token::RParen => {
3985                                self.advance();
3986                                break;
3987                            }
3988                            other => {
3989                                return Err(self.err(format!(
3990                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
3991                                )));
3992                            }
3993                        }
3994                    }
3995                }
3996                Ok(Statement::Analyze(target))
3997            }
3998            // v7.12.1 — `SET <name> [TO|=] <value>`. The
3999            // `default_text_search_config` parameter is consumed
4000            // by the FTS function dispatcher; other parameter
4001            // names are recorded but treated as a no-op so PG
4002            // dump output loads.
4003            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
4004                self.advance();
4005                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
4006                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
4007                // …` which the SessionVar path handles). `LOCAL` is the only
4008                // one that changes semantics — it scopes the change to the
4009                // current transaction — so capture it; SESSION / GLOBAL are
4010                // accepted and treated as the default session scope.
4011                let mut set_local = false;
4012                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
4013                    let q = s.to_ascii_lowercase();
4014                    if q == "local" || q == "session" || q == "global" {
4015                        set_local = q == "local";
4016                        self.advance();
4017                    }
4018                }
4019                // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
4020                // { DEFAULT | <role> }`. pg_dump's ACL section switches
4021                // to the object owner with it. SPG maps it onto the
4022                // session-role machinery (recorded delta RD-10: PG moves
4023                // session_user too; SPG moves the effective role).
4024                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4025                    if s.eq_ignore_ascii_case("authorization"))
4026                {
4027                    self.advance(); // AUTHORIZATION
4028                    let role = match self.peek().clone() {
4029                        Token::Default => {
4030                            self.advance();
4031                            None
4032                        }
4033                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4034                            self.advance();
4035                            Some(s)
4036                        }
4037                        _ => None,
4038                    };
4039                    return Ok(Statement::SetRole(role));
4040                }
4041                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
4042                // <collation>]` — change the connection client
4043                // charset. SPG stores UTF-8 always and orders
4044                // bytewise; accept as a no-op.
4045                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
4046                {
4047                    self.advance();
4048                    // v7.39 — this used to parse the clause and throw it
4049                    // away ("SPG stores UTF-8 always and orders
4050                    // bytewise; accept as a no-op"). That sentence
4051                    // stopped being true when collations arrived, and
4052                    // once `collation_connection` began driving literal
4053                    // comparison, dropping the COLLATE clause became a
4054                    // silently wrong answer: `SET NAMES utf8mb4 COLLATE
4055                    // utf8mb4_general_ci` reported back
4056                    // `utf8mb4_0900_ai_ci` and compared as NO PAD.
4057                    //
4058                    // The charset name is emitted as `names` and the
4059                    // ENGINE expands it, because which collation a
4060                    // charset defaults to is MySQL semantics and belongs
4061                    // beside the rest of them, not in the parser.
4062                    let mut pairs = alloc::vec::Vec::new();
4063                    if matches!(
4064                        self.peek(),
4065                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4066                    ) {
4067                        let charset = match self.advance() {
4068                            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4069                            _ => unreachable!("peeked an ident-or-string"),
4070                        };
4071                        pairs.push((String::from("names"), crate::ast::SetValue::Ident(charset)));
4072                    }
4073                    // Optional `COLLATE <name>` — emitted AFTER `names`
4074                    // so it overrides the charset's default, which is
4075                    // what MySQL does.
4076                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4077                    {
4078                        self.advance();
4079                        if matches!(
4080                            self.peek(),
4081                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4082                        ) {
4083                            let coll = match self.advance() {
4084                                Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4085                                _ => unreachable!("peeked an ident-or-string"),
4086                            };
4087                            pairs.push((
4088                                String::from("collation_connection"),
4089                                crate::ast::SetValue::Ident(coll),
4090                            ));
4091                        }
4092                    }
4093                    if pairs.is_empty() {
4094                        return Ok(Statement::Empty);
4095                    }
4096                    return Ok(Statement::SetParameterList(pairs));
4097                }
4098                // v7.37.17 (17.6 sibling) — PG `SET ROLE
4099                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4100                // uses this to switch to the object owner before
4101                // recreating tables. SPG has no role system so this
4102                // is a no-op.
4103                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4104                {
4105                    self.advance(); // ROLE
4106                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4107                    // reset to the login identity; a name / string sets the
4108                    // effective role that drives current_user + RLS.
4109                    let role = match self.peek().clone() {
4110                        Token::Default => {
4111                            self.advance();
4112                            None
4113                        }
4114                        Token::Ident(s) | Token::QuotedIdent(s)
4115                            if s.eq_ignore_ascii_case("none") =>
4116                        {
4117                            self.advance();
4118                            None
4119                        }
4120                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4121                            self.advance();
4122                            Some(s)
4123                        }
4124                        _ => None,
4125                    };
4126                    return Ok(Statement::SetRole(role));
4127                }
4128                // v7.37.17 (17.6 sibling) — PG `SET SESSION
4129                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4130                // ISO SQL surface). pg_dump prepends this to fix
4131                // the isolation level for the restore session. SPG
4132                // defaults to READ COMMITTED and doesn't yet honor
4133                // session-set isolation across statements — accept
4134                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4135                // per-tx form is handled elsewhere.
4136                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4137                {
4138                    self.advance(); // CHARACTERISTICS
4139                    if matches!(self.peek(), Token::As) {
4140                        self.advance();
4141                    }
4142                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
4143                        self.advance();
4144                    }
4145                    // v7.39 — no longer a no-op. The note above said SPG
4146                    // "doesn't yet honor session-set isolation across
4147                    // statements"; it does now, through
4148                    // `default_transaction_isolation`, and measured on
4149                    // PG 18.6 this statement is exactly a way to set it:
4150                    //
4151                    //   SET SESSION CHARACTERISTICS AS TRANSACTION
4152                    //       ISOLATION LEVEL REPEATABLE READ;
4153                    //   current_setting('default_transaction_isolation')
4154                    //       -> repeatable read
4155                    //
4156                    // pg_dump prepends this to fix the level for a
4157                    // restore session, so accepting it and doing nothing
4158                    // meant the restore ran at a level nobody chose.
4159                    //
4160                    // The trailing READ ONLY / [NOT] DEFERRABLE modes are
4161                    // still consumed and dropped. `default_transaction_read_only`
4162                    // exists in the GUC inventory but nothing enforces it,
4163                    // and setting a value no code honours is the very
4164                    // defect this version is about — a session told it
4165                    // holds a guarantee it does not.
4166                    let modes = self.parse_isolation_level_clauses()?;
4167                    self.consume_until_statement_boundary();
4168                    let mut pairs: alloc::vec::Vec<(
4169                        alloc::string::String,
4170                        crate::ast::SetValue,
4171                    )> = alloc::vec::Vec::new();
4172                    if let Some(level) = modes.isolation {
4173                        pairs.push((
4174                            alloc::string::String::from("default_transaction_isolation"),
4175                            crate::ast::SetValue::String(alloc::string::String::from(
4176                                level.as_pg_str(),
4177                            )),
4178                        ));
4179                    }
4180                    if let Some(ro) = modes.read_only {
4181                        pairs.push((
4182                            alloc::string::String::from("default_transaction_read_only"),
4183                            crate::ast::SetValue::Ident(alloc::string::String::from(if ro {
4184                                "on"
4185                            } else {
4186                                "off"
4187                            })),
4188                        ));
4189                    }
4190                    return Ok(if pairs.is_empty() {
4191                        Statement::Empty
4192                    } else {
4193                        Statement::SetParameterList(pairs)
4194                    });
4195                }
4196                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4197                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4198                // pg_dump emits this to control the deferrability of
4199                // FK / UNIQUE constraints across a bulk restore. SPG
4200                // has no deferrable-constraint machinery today; the
4201                // FK checker is strict-immediate. Accept-and-no-op
4202                // for pg_dump round-trip compatibility.
4203                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4204                {
4205                    self.advance(); // CONSTRAINTS
4206                    // v7.39 (round 288) — no longer a no-op: the trailing
4207                    // DEFERRED / IMMEDIATE sets the transaction's timing.
4208                    // v7.39 (round 308, V29) — and the names are kept.
4209                    // They used to be skipped over on the way to the
4210                    // DEFERRED keyword, so a named form silently behaved
4211                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4212                    // every deferrable constraint in the transaction.
4213                    let mut names: alloc::vec::Vec<alloc::string::String> =
4214                        alloc::vec::Vec::new();
4215                    if matches!(self.peek(), Token::All) {
4216                        self.advance();
4217                    } else {
4218                        loop {
4219                            let mut n = self.expect_ident_like()?;
4220                            // A schema-qualified name (`public.fk_a`)
4221                            // identifies the same constraint; PG resolves
4222                            // it by the trailing segment.
4223                            while matches!(self.peek(), Token::Dot) {
4224                                self.advance();
4225                                n = self.expect_ident_like()?;
4226                            }
4227                            names.push(n);
4228                            if matches!(self.peek(), Token::Comma) {
4229                                self.advance();
4230                            } else {
4231                                break;
4232                            }
4233                        }
4234                    }
4235                    let deferred = match self.peek() {
4236                        Token::Ident(s) | Token::QuotedIdent(s)
4237                            if s.eq_ignore_ascii_case("deferred") =>
4238                        {
4239                            true
4240                        }
4241                        Token::Ident(s) | Token::QuotedIdent(s)
4242                            if s.eq_ignore_ascii_case("immediate") =>
4243                        {
4244                            false
4245                        }
4246                        other => {
4247                            return Err(self.err(alloc::format!(
4248                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4249                            )));
4250                        }
4251                    };
4252                    self.advance();
4253                    return Ok(Statement::SetConstraints { names, deferred });
4254                }
4255                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4256                // { DEFAULT | '<role>' | <ident> }` (mailrs
4257                // round-10 A.1). pg_dump preamble emits the
4258                // `DEFAULT` form to reset session authorization.
4259                //
4260                // v7.39 (round 697) — this said "SPG has no role system so
4261                // this is a strict no-op". SPG has had one since round 58;
4262                // the comment outlived it, and with it the reason a name
4263                // that is not a role was accepted here. It still switches
4264                // no authorization — what it does now is refuse a role
4265                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4266                // AUTHORIZATION` (handled by the RESET parser
4267                // elsewhere). Reference:
4268                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4269                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4270                {
4271                    self.advance(); // AUTHORIZATION
4272                    match self.peek().clone() {
4273                        Token::Default => {
4274                            self.advance();
4275                        }
4276                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4277                            self.advance();
4278                            return Ok(Statement::ValidateOnly {
4279                                kind: crate::ast::ValidateOnlyKind::RoleName,
4280                                names: alloc::vec![r],
4281                            });
4282                        }
4283                        other => {
4284                            return Err(self.err(alloc::format!(
4285                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4286                            )));
4287                        }
4288                    }
4289                    return Ok(Statement::Empty);
4290                }
4291                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4292                // ISOLATION LEVEL { READ COMMITTED | READ
4293                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4294                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4295                // PG-standard surface. v7.37.8 accepts the syntax
4296                // and tracks the selected level on
4297                // `Engine::current_isolation_level()`; the actual
4298                // MVCC / SSI semantics implementation lands in
4299                // the 轴 4 isolation framework (separate train).
4300                // PG itself maps READ UNCOMMITTED to READ COMMITTED
4301                // internally; SPG behaves the same (effectively
4302                // READ COMMITTED at every level today).
4303                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4304                {
4305                    self.advance(); // TRANSACTION
4306                    let modes = self.parse_isolation_level_clauses()?;
4307                    return Ok(Statement::SetTransaction { modes });
4308                }
4309                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4310                // alias — same accept-as-no-op as SET NAMES.
4311                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4312                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4313                {
4314                    self.advance(); // CHARACTER
4315                    self.advance(); // SET
4316                    if matches!(
4317                        self.peek(),
4318                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4319                    ) {
4320                        self.advance();
4321                    }
4322                    return Ok(Statement::Empty);
4323                }
4324                // v7.39 (GUC) — PG spells the timezone GUC as two
4325                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4326                // where <value> is a string/ident or the LOCAL /
4327                // DEFAULT keyword (both mean "back to the default").
4328                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4329                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4330                {
4331                    self.advance(); // TIME
4332                    self.advance(); // ZONE
4333                    let value = match self.peek().clone() {
4334                        Token::Ident(s)
4335                            if s.eq_ignore_ascii_case("local")
4336                                || s.eq_ignore_ascii_case("default") =>
4337                        {
4338                            self.advance();
4339                            crate::ast::SetValue::Default
4340                        }
4341                        Token::Default => {
4342                            self.advance();
4343                            crate::ast::SetValue::Default
4344                        }
4345                        _ => self.parse_set_value()?,
4346                    };
4347                    return Ok(Statement::SetParameter {
4348                        name: "timezone".into(),
4349                        value,
4350                        local: set_local,
4351                    });
4352                }
4353                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4354                // MySQL USER-variable assignment: its own per-session
4355                // namespace, an arbitrary expression on the right, and `:=`
4356                // as a second spelling of `=`. It used to fall into the
4357                // session-PARAMETER list below, whose values are literals and
4358                // whose store nothing reads back under a `@` name — so the
4359                // assignment reported success and vanished.
4360                //
4361                // A `@@`-prefixed LHS is a real engine setting and keeps the
4362                // old path.
4363                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4364                    return self.parse_set_user_vars();
4365                }
4366                // v7.14.0 — multi-assignment form
4367                // `SET a = 1, b = 2, …`. Single-assignment is the
4368                // 1-element case. Each LHS may be a regular ident
4369                // or a SessionVar (`@VAR` / `@@VAR`).
4370                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4371                loop {
4372                    let lhs = match self.peek().clone() {
4373                        Token::SessionVar(s) => {
4374                            self.advance();
4375                            s
4376                        }
4377                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4378                        other => {
4379                            return Err(self.err(format!(
4380                                "expected parameter name after SET, got {other:?}"
4381                            )));
4382                        }
4383                    };
4384                    // Accept either `=` or the bare `TO` keyword.
4385                    match self.peek() {
4386                        Token::Eq => {
4387                            self.advance();
4388                        }
4389                        Token::To => {
4390                            self.advance();
4391                        }
4392                        other => {
4393                            return Err(self.err(format!(
4394                                "expected `=` or TO after SET {lhs}, got {other:?}"
4395                            )));
4396                        }
4397                    }
4398                    let mut value = self.parse_set_value()?;
4399                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4400                    // `, name TO` continues a MySQL-style multi-assign,
4401                    // anything else is a PG list VALUE
4402                    // (`SET search_path = myschema, public`) folded into
4403                    // one comma-joined string.
4404                    while matches!(self.peek(), Token::Comma) {
4405                        let is_assign = matches!(
4406                            self.tokens.get(self.pos + 1),
4407                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4408                        ) && matches!(
4409                            self.tokens.get(self.pos + 2),
4410                            Some(Token::Eq | Token::To)
4411                        );
4412                        if is_assign {
4413                            break;
4414                        }
4415                        self.advance(); // comma
4416                        let next = self.parse_set_value()?;
4417                        let joined = alloc::format!(
4418                            "{}, {}",
4419                            set_value_text(&value),
4420                            set_value_text(&next)
4421                        );
4422                        value = crate::ast::SetValue::String(joined);
4423                    }
4424                    pairs.push((lhs, value));
4425                    if matches!(self.peek(), Token::Comma) {
4426                        self.advance();
4427                        continue;
4428                    }
4429                    break;
4430                }
4431                if pairs.len() == 1 {
4432                    let (name, value) = pairs.into_iter().next().unwrap();
4433                    Ok(Statement::SetParameter {
4434                        name,
4435                        value,
4436                        local: set_local,
4437                    })
4438                } else {
4439                    Ok(Statement::SetParameterList(pairs))
4440                }
4441            }
4442            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4443            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4444                self.advance();
4445                match self.peek().clone() {
4446                    Token::All => {
4447                        self.advance();
4448                        Ok(Statement::ResetParameter(None))
4449                    }
4450                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4451                        self.advance();
4452                        Ok(Statement::ResetParameter(None))
4453                    }
4454                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4455                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4456                        self.advance();
4457                        Ok(Statement::SetRole(None))
4458                    }
4459                    // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4460                    // (pg_dump's return from the owner switch).
4461                    Token::Ident(s) | Token::QuotedIdent(s)
4462                        if s.eq_ignore_ascii_case("session")
4463                            && matches!(
4464                                self.tokens.get(self.pos + 1),
4465                                Some(Token::Ident(a) | Token::QuotedIdent(a))
4466                                    if a.eq_ignore_ascii_case("authorization")
4467                            ) =>
4468                    {
4469                        self.advance(); // SESSION
4470                        self.advance(); // AUTHORIZATION
4471                        Ok(Statement::SetRole(None))
4472                    }
4473                    _ => {
4474                        let name = self.parse_set_param_name()?;
4475                        Ok(Statement::ResetParameter(Some(name)))
4476                    }
4477                }
4478            }
4479            // v7.39 (round 218) — server-side cursors.
4480            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4481            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4482            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4483            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4484                self.advance();
4485                match self.peek().clone() {
4486                    Token::All => {
4487                        self.advance();
4488                        Ok(Statement::CloseCursor { name: None })
4489                    }
4490                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4491                        self.advance();
4492                        Ok(Statement::CloseCursor { name: None })
4493                    }
4494                    Token::Ident(n) | Token::QuotedIdent(n) => {
4495                        self.advance();
4496                        Ok(Statement::CloseCursor { name: Some(n) })
4497                    }
4498                    other => Err(self.err(format!(
4499                        "expected cursor name or ALL after CLOSE, got {other:?}"
4500                    ))),
4501                }
4502            }
4503            other => Err(self.err(format!(
4504                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4505                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4506            ))),
4507        }
4508    }
4509
4510    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4511    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4512    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4513    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4514    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4515        self.advance(); // DECLARE
4516        let name = match self.advance() {
4517            Token::Ident(n) | Token::QuotedIdent(n) => n,
4518            other => {
4519                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4520            }
4521        };
4522        let mut scroll: Option<bool> = None;
4523        loop {
4524            match self.peek() {
4525                Token::Ident(s)
4526                    if s.eq_ignore_ascii_case("binary")
4527                        || s.eq_ignore_ascii_case("insensitive")
4528                        || s.eq_ignore_ascii_case("asensitive") =>
4529                {
4530                    self.advance();
4531                }
4532                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4533                    self.advance();
4534                    scroll = Some(true);
4535                }
4536                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4537                {
4538                    self.advance(); // NO
4539                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4540                        return Err(self.err(format!(
4541                            "expected SCROLL after NO in DECLARE, got {:?}",
4542                            self.peek()
4543                        )));
4544                    }
4545                    self.advance();
4546                    scroll = Some(false);
4547                }
4548                _ => break,
4549            }
4550        }
4551        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4552            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4553        }
4554        self.advance();
4555        let mut hold = false;
4556        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4557            self.advance();
4558            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4559                return Err(self.err(format!(
4560                    "expected HOLD after WITH in DECLARE, got {:?}",
4561                    self.peek()
4562                )));
4563            }
4564            self.advance();
4565            hold = true;
4566        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4567            self.advance();
4568            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4569                return Err(self.err(format!(
4570                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4571                    self.peek()
4572                )));
4573            }
4574            self.advance();
4575        }
4576        if !matches!(self.peek(), Token::For) {
4577            return Err(self.err(format!(
4578                "expected FOR before the cursor query, got {:?}",
4579                self.peek()
4580            )));
4581        }
4582        self.advance();
4583        let query = self.parse_one_statement()?;
4584        Ok(Statement::DeclareCursor {
4585            name,
4586            scroll,
4587            hold,
4588            query: alloc::boxed::Box::new(query),
4589        })
4590    }
4591
4592    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4593    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4594    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4595    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4596        use crate::ast::CursorDirection as D;
4597        self.advance(); // FETCH / MOVE
4598        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4599            let neg = if matches!(this.peek(), Token::Minus) {
4600                this.advance();
4601                true
4602            } else {
4603                false
4604            };
4605            match this.advance() {
4606                Token::Integer(v) => Ok(if neg { -v } else { v }),
4607                other => Err(this.err(format!("expected count, got {other:?}"))),
4608            }
4609        };
4610        let direction = match self.peek().clone() {
4611            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4612                self.advance();
4613                D::Next
4614            }
4615            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4616                self.advance();
4617                D::Prior
4618            }
4619            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4620                self.advance();
4621                D::First
4622            }
4623            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4624                self.advance();
4625                D::Last
4626            }
4627            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4628                self.advance();
4629                D::Absolute(signed_count(self)?)
4630            }
4631            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4632                self.advance();
4633                D::Relative(signed_count(self)?)
4634            }
4635            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4636                self.advance();
4637                match self.peek().clone() {
4638                    Token::All => {
4639                        self.advance();
4640                        D::All
4641                    }
4642                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4643                        self.advance();
4644                        D::All
4645                    }
4646                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4647                    _ => D::Next, // bare FORWARD = FORWARD 1
4648                }
4649            }
4650            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4651                self.advance();
4652                match self.peek().clone() {
4653                    Token::All => {
4654                        self.advance();
4655                        D::BackwardAll
4656                    }
4657                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4658                        self.advance();
4659                        D::BackwardAll
4660                    }
4661                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4662                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4663                }
4664            }
4665            Token::All => {
4666                self.advance();
4667                D::All
4668            }
4669            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4670                self.advance();
4671                D::All
4672            }
4673            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4674            // Bare `FETCH <name>` — direction defaults to NEXT.
4675            _ => D::Next,
4676        };
4677        // Optional FROM / IN.
4678        if matches!(self.peek(), Token::From)
4679            || matches!(self.peek(), Token::In)
4680            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4681        {
4682            self.advance();
4683        }
4684        let name = match self.advance() {
4685            Token::Ident(n) | Token::QuotedIdent(n) => n,
4686            other => {
4687                return Err(self.err(format!("expected cursor name, got {other:?}")));
4688            }
4689        };
4690        Ok(if is_move {
4691            Statement::MoveCursor { name, direction }
4692        } else {
4693            Statement::FetchCursor { name, direction }
4694        })
4695    }
4696
4697    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4698    /// [(kind, …)] ON <col>, … FROM <table>`.
4699    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4700        self.advance(); // STATISTICS
4701        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4702        let mut if_not_exists = false;
4703        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4704            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4705        {
4706            self.advance();
4707            self.advance();
4708            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4709                self.advance();
4710                if_not_exists = true;
4711            }
4712        }
4713        let name = self.expect_ident_like()?;
4714        let mut kinds = Vec::new();
4715        if matches!(self.peek(), Token::LParen) {
4716            self.advance();
4717            loop {
4718                let k = self.expect_ident_like()?;
4719                // PG stores the single letters; accept the spelled-out
4720                // names the SQL uses and record what PG records.
4721                kinds.push(match k.to_ascii_lowercase().as_str() {
4722                    "ndistinct" => String::from("d"),
4723                    "dependencies" => String::from("f"),
4724                    "mcv" => String::from("m"),
4725                    other => {
4726                        return Err(
4727                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4728                        );
4729                    }
4730                });
4731                match self.advance() {
4732                    Token::Comma => {}
4733                    Token::RParen => break,
4734                    other => {
4735                        return Err(self.err(alloc::format!(
4736                            "expected ',' or ')' in statistics kind list, got {other:?}"
4737                        )));
4738                    }
4739                }
4740            }
4741        }
4742        if !matches!(self.peek(), Token::On) {
4743            return Err(self.err(alloc::format!(
4744                "expected ON in CREATE STATISTICS, got {:?}",
4745                self.peek()
4746            )));
4747        }
4748        self.advance();
4749        let mut columns = Vec::new();
4750        loop {
4751            columns.push(self.expect_ident_like()?);
4752            if matches!(self.peek(), Token::Comma) {
4753                self.advance();
4754            } else {
4755                break;
4756            }
4757        }
4758        if !matches!(self.peek(), Token::From) {
4759            return Err(self.err(alloc::format!(
4760                "expected FROM in CREATE STATISTICS, got {:?}",
4761                self.peek()
4762            )));
4763        }
4764        self.advance();
4765        let table = self.expect_ident_like()?;
4766        Ok(Statement::CreateStatistics {
4767            name,
4768            if_not_exists,
4769            kinds,
4770            columns,
4771            table,
4772        })
4773    }
4774
4775    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4776    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4777    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4778    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4779    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4780    /// forward call.
4781    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4782        self.advance(); // TABLE
4783        let if_exists = self.consume_if_exists();
4784        let mut names: Vec<String> = Vec::new();
4785        loop {
4786            names.push(self.expect_ident_like()?);
4787            if matches!(self.peek(), Token::Comma) {
4788                self.advance();
4789                continue;
4790            }
4791            break;
4792        }
4793        if matches!(
4794            self.peek(),
4795            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4796                || s.eq_ignore_ascii_case("restrict")
4797        ) {
4798            self.advance();
4799        }
4800        Ok(Statement::DropTable { names, if_exists })
4801    }
4802
4803    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4804        self.advance(); // STATISTICS
4805        let mut if_exists = false;
4806        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4807            && matches!(self.tokens.get(self.pos + 1),
4808                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4809        {
4810            self.advance();
4811            self.advance();
4812            if_exists = true;
4813        }
4814        let name = self.expect_ident_like()?;
4815        Ok(Statement::DropStatistics { name, if_exists })
4816    }
4817
4818    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4819        debug_assert!(matches!(self.peek(), Token::Create));
4820        self.advance();
4821        match self.peek() {
4822            Token::Table => self.parse_create_table_stmt_after_create(),
4823            Token::Index => self.parse_create_index_stmt_after_create(false),
4824            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4825            // object now. It used to be consumed by the CREATE-noise
4826            // arm, so a pg_dump that declares extended statistics
4827            // restored silently without them and reflection showed
4828            // nothing.
4829            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4830                self.parse_create_statistics_after_create()
4831            }
4832            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4833            // The `UNIQUE` modifier turns a partial index into a
4834            // partial-uniqueness invariant (only rows matching the
4835            // WHERE predicate are checked for duplicates). mailrs
4836            // K1 (3 hits: email_templates default, calendar_events
4837            // master, calendar_events instance).
4838            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4839                self.advance();
4840                if !matches!(self.peek(), Token::Index) {
4841                    return Err(self.err(alloc::format!(
4842                        "expected INDEX after CREATE UNIQUE, got {:?}",
4843                        self.peek()
4844                    )));
4845                }
4846                self.parse_create_index_stmt_after_create(true)
4847            }
4848            Token::Publication => {
4849                self.advance();
4850                self.parse_create_publication_after_keyword()
4851            }
4852            Token::Subscription => {
4853                self.advance();
4854                self.parse_create_subscription_after_keyword()
4855            }
4856            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4857            // USER isn't a reserved keyword — we look for the bare
4858            // identifier so the lexer doesn't have to grow a token.
4859            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4860                self.advance();
4861                self.parse_create_user_after_keyword(true)
4862            }
4863            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4864            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4865            // the default of the LOGIN attribute.
4866            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4867                self.advance();
4868                self.parse_create_user_after_keyword(false)
4869            }
4870            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4871            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4872                self.advance();
4873                self.parse_create_policy_after_keyword()
4874            }
4875            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4876            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4877            // no-op. mailrs follow-up F3.
4878            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4879                self.advance();
4880                self.parse_create_extension_after_keyword()
4881            }
4882            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4883            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4884            // optional; absorb it here and forward to the
4885            // per-kind parsers with the flag. OR is a reserved
4886            // keyword token.
4887            Token::Or => {
4888                self.advance();
4889                let next = self.peek();
4890                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4891                    return Err(self.err(alloc::format!(
4892                        "expected REPLACE after CREATE OR, got {next:?}"
4893                    )));
4894                };
4895                if !s2.eq_ignore_ascii_case("replace") {
4896                    return Err(self.err(alloc::format!(
4897                        "expected REPLACE after CREATE OR, got {s2:?}"
4898                    )));
4899                }
4900                self.advance();
4901                self.parse_create_function_or_trigger_after_or_replace(true)
4902            }
4903            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4904                self.advance();
4905                self.parse_create_function_after_keyword(false)
4906            }
4907            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4908                self.advance();
4909                self.parse_create_trigger_after_keyword(false)
4910            }
4911            // v7.39 (round 139) — CREATE RULE …
4912            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4913                self.advance();
4914                self.parse_create_rule_after_keyword(false)
4915            }
4916            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4917            // trigger is a row-level AFTER trigger that additionally carries
4918            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4919            // path already tolerates and skips those clauses, so consuming the
4920            // CONSTRAINT keyword and reusing it makes the statement parse and the
4921            // trigger fire. (The deferral timing itself is not yet honoured —
4922            // SPG fires it as a plain AFTER trigger, which is correct behaviour
4923            // for every non-deferred use.)
4924            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4925                self.advance();
4926                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4927                    if t.eq_ignore_ascii_case("trigger"))
4928                {
4929                    return Err(self.err(alloc::format!(
4930                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4931                        self.peek()
4932                    )));
4933                }
4934                self.advance();
4935                self.parse_create_trigger_after_keyword(false)
4936            }
4937            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4938            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4939                self.advance();
4940                self.parse_create_sequence_after_keyword(false)
4941            }
4942            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4943            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4944                self.advance();
4945                self.parse_create_view_after_keyword(false, false, false)
4946            }
4947            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4948            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4949            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4950            // appear (in any order) between `CREATE` and `VIEW` in
4951            // every mysqldump-emitted view. Pre-2.6 the parser
4952            // rejected the prefix and the customer's whole view
4953            // backup failed on the first view. The hints are pure
4954            // planner / permission metadata; SPG's view-rewrite
4955            // path is semantically equivalent for all three
4956            // algorithms in v7.17 (TEMPTABLE differs only in
4957            // perf for huge views — out of v7.17 scope), and
4958            // DEFINER / SQL SECURITY are pure single-user
4959            // permissioning that SPG ignores by design.
4960            Token::Ident(s) | Token::QuotedIdent(s)
4961                if s.eq_ignore_ascii_case("algorithm")
4962                    || s.eq_ignore_ascii_case("definer")
4963                    || s.eq_ignore_ascii_case("sql") =>
4964            {
4965                self.consume_mysql_view_prefix()?;
4966                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4967                // (in any order, in any combination), the next
4968                // keyword must be VIEW. mysqldump never emits these
4969                // prefixes on non-view statements.
4970                let next = self.peek().clone();
4971                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4972                    if s2.eq_ignore_ascii_case("view"))
4973                {
4974                    self.advance();
4975                    self.parse_create_view_after_keyword(false, false, false)
4976                } else {
4977                    Err(self.err(alloc::format!(
4978                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4979                    )))
4980                }
4981            }
4982            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4983            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4984                self.advance();
4985                self.parse_create_type_after_keyword()
4986            }
4987            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4988            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4989            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4990                self.advance();
4991                self.parse_create_domain_after_keyword()
4992            }
4993            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4994            // name [AUTHORIZATION user]. Real catalog registry
4995            // (was silent-no-op'd pre-v7.17).
4996            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4997                self.advance();
4998                let if_not_exists = self.parse_if_not_exists();
4999                let name = self.expect_ident_like()?;
5000                // Optional `AUTHORIZATION <user>` trailer — accepted,
5001                // ignored (single-user catalog).
5002                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
5003                    if s.eq_ignore_ascii_case("authorization"))
5004                {
5005                    self.advance();
5006                    let _ = self.expect_ident_like()?;
5007                }
5008                Ok(Statement::CreateSchema { name, if_not_exists })
5009            }
5010            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
5011            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
5012                self.advance();
5013                let next = self.peek().clone();
5014                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5015                {
5016                    self.advance();
5017                    self.parse_create_materialized_view_after_keyword()
5018                } else {
5019                    Err(self.err(alloc::format!(
5020                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
5021                    )))
5022                }
5023            }
5024            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
5025            // no-op below), an UNLOGGED table is a real, fully-usable table in
5026            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
5027            // durability optimisation is a follow-up), so a dump / app that
5028            // declares UNLOGGED tables works instead of failing to parse.
5029            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
5030                self.advance(); // UNLOGGED
5031                if matches!(self.peek(), Token::Table) {
5032                    self.parse_create_table_stmt_after_create()
5033                } else {
5034                    Err(self.err(format!(
5035                        "expected TABLE after CREATE UNLOGGED, got {:?}",
5036                        self.peek()
5037                    )))
5038                }
5039            }
5040            Token::Ident(s) | Token::QuotedIdent(s)
5041                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
5042            {
5043                self.advance();
5044                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
5045                let next = self.peek().clone();
5046                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
5047                {
5048                    self.advance();
5049                    self.parse_create_sequence_after_keyword(true)
5050                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5051                {
5052                    self.advance();
5053                    self.parse_create_view_after_keyword(false, false, true)
5054                } else {
5055                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
5056                    // consumed and answered OK while creating nothing, so
5057                    // every statement that touched the table afterwards failed
5058                    // with "table not found" — the DDL itself lied. It is a
5059                    // real CREATE TABLE now, marked temporary so the executor
5060                    // puts it in the session's own namespace. An optional
5061                    // TABLE keyword may or may not be present (`CREATE TEMP t`
5062                    // is not legal, but the keyword is consumed by the
5063                    // CREATE TABLE parser itself).
5064                    let stmt = self.parse_create_table_stmt_after_create()?;
5065                    match stmt {
5066                        Statement::CreateTable(mut c) => {
5067                            c.temporary = true;
5068                            Ok(Statement::CreateTable(c))
5069                        }
5070                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
5071                        // CTAS node, which needs the same session namespace.
5072                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
5073                            m.temporary = true;
5074                            Ok(Statement::CreateMaterializedView(m))
5075                        }
5076                        other => Ok(other),
5077                    }
5078                }
5079            }
5080            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
5081            // BEGIN <body> END`. The body may reference `@var`
5082            // session variables, SET statements, internal `;`
5083            // terminators, etc. SPG has no procedure runtime, so
5084            // consume the whole `CREATE PROCEDURE … END` block as
5085            // a no-op so mysqldump scripts that include stored
5086            // routines load through. The matching-END consumer
5087            // tracks BEGIN/END nesting depth to handle nested
5088            // BEGIN blocks correctly.
5089            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
5090                self.consume_mysql_routine_body();
5091                Ok(Statement::Empty)
5092            }
5093            // v7.14.0 — pg_dump / mysqldump emit
5094            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
5095            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
5096            // SPG is single-schema / single-database; these have
5097            // no behavioural effect, so consume + return Empty.
5098            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
5099            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
5100            // moved up to real parser branches. DATABASE / ROLE /
5101            // POLICY / OPERATOR stay no-op forever
5102            // (single-database, hardcoded roles).
5103            Token::Ident(s) | Token::QuotedIdent(s)
5104                if matches!(
5105                    s.to_ascii_lowercase().as_str(),
5106                    "database"
5107                        | "role"
5108                        | "operator"
5109                        | "cast"
5110                        | "aggregate"
5111                        | "language"
5112                        | "collation"
5113                        | "conversion"
5114                        // v7.17.0 Phase 8 (audit N6) — rarely-
5115                        // emitted pg_dump shapes that should
5116                        // load through without a parser error.
5117                        // SPG has no planner statistics catalog,
5118                        // no event-trigger hooks, no foreign-
5119                        // data-wrapper infrastructure; consume
5120                        // + return Empty.
5121                        | "statistics"
5122                        | "event"
5123                        // v7.37.17 (17.6 siblings) — additional CREATE
5124                        // targets pg_dump / operator install scripts
5125                        // may emit that SPG has no matching machinery
5126                        // for. Consume + Empty-return.
5127                        | "text"
5128                        | "tablespace"
5129                        | "access"
5130                        | "large"
5131                ) =>
5132            {
5133                // DATABASE is the one member of this list PG refuses
5134                // inside a transaction block; the rest (ROLE, CAST,
5135                // TABLESPACE, …) it runs there quite happily, so only
5136                // this one is named. Still a no-op otherwise — SPG is
5137                // single-database.
5138                let is_database = s.eq_ignore_ascii_case("database");
5139                // The name is the first token after DATABASE, past an
5140                // `IF NOT EXISTS`.
5141                let name = if is_database {
5142                    self.scan_database_name()
5143                } else {
5144                    None
5145                };
5146                let collation = if is_database {
5147                    self.scan_database_collation_until_boundary()
5148                } else {
5149                    self.consume_until_statement_boundary();
5150                    None
5151                };
5152                if is_database {
5153                    return Ok(Statement::NoOpPreventedInTransaction {
5154                        what: String::from("CREATE DATABASE"),
5155                        collation,
5156                        name,
5157                    });
5158                }
5159                Ok(Statement::Empty)
5160            }
5161            // v7.39 (round 706) — the foreign-data family leaves the silent
5162            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5163            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5164            // FDW machinery), but the ENGINE now warns, so a restore log
5165            // says what will not function instead of reporting success.
5166            Token::Ident(s) | Token::QuotedIdent(s)
5167                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5168            {
5169                self.consume_until_statement_boundary();
5170                Ok(Statement::ValidateOnly {
5171                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5172                    names: Vec::new(),
5173                })
5174            }
5175            other => Err(self.err(format!(
5176                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5177            ))),
5178        }
5179    }
5180
5181    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5182    /// keyword decides whether we parse a function or trigger
5183    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5184    /// PROCEDURE) — those land in later releases.
5185    fn parse_create_function_or_trigger_after_or_replace(
5186        &mut self,
5187        or_replace: bool,
5188    ) -> Result<Statement, ParseError> {
5189        let tok = self.peek();
5190        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5191            return Err(self.err(alloc::format!(
5192                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5193            )));
5194        };
5195        if s.eq_ignore_ascii_case("function") {
5196            self.advance();
5197            self.parse_create_function_after_keyword(or_replace)
5198        } else if s.eq_ignore_ascii_case("trigger") {
5199            self.advance();
5200            self.parse_create_trigger_after_keyword(or_replace)
5201        } else if s.eq_ignore_ascii_case("rule") {
5202            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5203            self.advance();
5204            self.parse_create_rule_after_keyword(or_replace)
5205        } else if s.eq_ignore_ascii_case("view") {
5206            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5207            self.advance();
5208            self.parse_create_view_after_keyword(or_replace, false, false)
5209        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5210            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5211            self.advance();
5212            let nxt = self.peek().clone();
5213            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5214            {
5215                self.advance();
5216                self.parse_create_view_after_keyword(or_replace, false, true)
5217            } else {
5218                Err(self.err(alloc::format!(
5219                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5220                )))
5221            }
5222        } else {
5223            Err(self.err(alloc::format!(
5224                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5225            )))
5226        }
5227    }
5228
5229    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5230    /// SPG doesn't have a registry; pgvector / similar are
5231    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5232    /// the syntax lets dual-target schemas keep the line.
5233    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5234        // Optional `IF NOT EXISTS`.
5235        self.consume_if_not_exists();
5236        let name = self.expect_ident_like()?;
5237        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5238        // CASCADE / FROM '<v>' clauses; we don't model them.
5239        loop {
5240            match self.peek() {
5241                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5242                    self.advance();
5243                    continue;
5244                }
5245                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5246                    self.advance();
5247                    let _ = self.expect_ident_like()?;
5248                    continue;
5249                }
5250                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5251                    self.advance();
5252                    // String or ident literal.
5253                    let _ = self.advance();
5254                    continue;
5255                }
5256                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5257                    self.advance();
5258                    let _ = self.advance();
5259                    continue;
5260                }
5261                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5262                    self.advance();
5263                    continue;
5264                }
5265                _ => break,
5266            }
5267        }
5268        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5269        // nosuch` reported success and `pg_extension` then did not list it,
5270        // which is the accept-and-do-nothing shape F31 exists to find.
5271        Ok(Statement::ValidateOnly {
5272            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5273            names: alloc::vec![name],
5274        })
5275    }
5276
5277    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5278    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5279    /// already been consumed by the caller. Grammar accepted:
5280    ///
5281    ///   name `(` arg-list `)`
5282    ///   `RETURNS` return-type
5283    ///   [ `LANGUAGE` ident ]
5284    ///   `AS` $$ body $$
5285    ///   [ `LANGUAGE` ident ]
5286    ///
5287    /// Either `LANGUAGE` position is allowed; PG accepts both.
5288    fn parse_create_function_after_keyword(
5289        &mut self,
5290        or_replace: bool,
5291    ) -> Result<Statement, ParseError> {
5292        let name = self.expect_ident_like()?;
5293        // Argument list. v7.12.4 commonly sees the empty `()`
5294        // (trigger functions); typed args parse and round-trip
5295        // but the executor only invokes nullary functions.
5296        if !matches!(self.peek(), Token::LParen) {
5297            return Err(self.err(alloc::format!(
5298                "expected '(' after function name {name:?}, got {:?}",
5299                self.peek()
5300            )));
5301        }
5302        self.advance();
5303        let args = self.parse_function_arg_list()?;
5304        // RETURNS clause.
5305        let tok = self.peek();
5306        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5307            return Err(self.err(alloc::format!(
5308                "expected RETURNS after function arg list, got {tok:?}"
5309            )));
5310        };
5311        if !s.eq_ignore_ascii_case("returns") {
5312            return Err(self.err(alloc::format!(
5313                "expected RETURNS after function arg list, got {s:?}"
5314            )));
5315        }
5316        self.advance();
5317        let returns = self.parse_function_return()?;
5318        // Optional LANGUAGE clause (PG also accepts after AS — we'll
5319        // re-check after the body too).
5320        let mut language: Option<String> = self.parse_optional_language()?;
5321        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5322        // either side of the body and in any order, interleaved with
5323        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5324        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5325        // PG's own pg_dump output did not restore.
5326        let mut attrs = FunctionAttrs::default();
5327        loop {
5328            let before = self.pos;
5329            self.parse_function_attrs_into(&mut attrs)?;
5330            if language.is_none() {
5331                language = self.parse_optional_language()?;
5332            }
5333            if self.pos == before {
5334                break;
5335            }
5336        }
5337        // `AS` followed by a $$-quoted body (lexer already
5338        // collapses both `$$…$$` and `$tag$…$tag$` to a single
5339        // Token::String). AS is a reserved keyword (Token::As).
5340        if !matches!(self.peek(), Token::As) {
5341            return Err(self.err(alloc::format!(
5342                "expected AS before function body, got {:?}",
5343                self.peek()
5344            )));
5345        }
5346        self.advance();
5347        let body_text = match self.peek() {
5348            Token::String(s) => {
5349                let body = s.clone();
5350                self.advance();
5351                body
5352            }
5353            other => {
5354                return Err(self.err(alloc::format!(
5355                    "expected $$-quoted function body after AS, got {other:?}"
5356                )));
5357            }
5358        };
5359        // Trailing clauses — PG's other accepted position for both the
5360        // LANGUAGE and the attributes.
5361        loop {
5362            let before = self.pos;
5363            self.parse_function_attrs_into(&mut attrs)?;
5364            if language.is_none() {
5365                language = self.parse_optional_language()?;
5366            }
5367            if self.pos == before {
5368                break;
5369            }
5370        }
5371        let language = language.unwrap_or_else(|| String::from("sql"));
5372        // PL/pgSQL bodies get structure-parsed. Other languages
5373        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5374        // recognise) round-trip as Raw text — the executor errors
5375        // when invoked with a clear unsupported message.
5376        let body = if language.eq_ignore_ascii_case("plpgsql") {
5377            match parse_plpgsql_body(&body_text) {
5378                Ok(block) => FunctionBody::PlPgSql(block),
5379                // Best-effort: if the body parser doesn't yet
5380                // support a construct used inside, fall back to
5381                // raw — keeps `CREATE FUNCTION` itself working
5382                // (catalogue accepts), executor errors on
5383                // invocation only.
5384                Err(_) => FunctionBody::Raw(body_text),
5385            }
5386        } else {
5387            FunctionBody::Raw(body_text)
5388        };
5389        Ok(Statement::CreateFunction(CreateFunctionStatement {
5390            name,
5391            or_replace,
5392            args,
5393            returns,
5394            language,
5395            body,
5396            attrs,
5397        }))
5398    }
5399
5400    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5401    /// attribute clauses into `attrs`, stopping at the first token that
5402    /// is not one. Measured against PG 18.4, which accepts them in any
5403    /// order and on either side of the body.
5404    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5405        loop {
5406            let word = match self.peek() {
5407                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5408                // NOT LEAKPROOF — NOT is a reserved keyword token.
5409                Token::Not
5410                    if matches!(
5411                        self.tokens.get(self.pos + 1),
5412                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5413                    ) =>
5414                {
5415                    self.advance();
5416                    self.advance();
5417                    attrs.leakproof = false;
5418                    continue;
5419                }
5420                _ => return Ok(()),
5421            };
5422            match word.as_str() {
5423                "immutable" => {
5424                    self.advance();
5425                    attrs.volatility = FunctionVolatility::Immutable;
5426                }
5427                "stable" => {
5428                    self.advance();
5429                    attrs.volatility = FunctionVolatility::Stable;
5430                }
5431                "volatile" => {
5432                    self.advance();
5433                    attrs.volatility = FunctionVolatility::Volatile;
5434                }
5435                "strict" => {
5436                    self.advance();
5437                    attrs.strict = true;
5438                }
5439                "leakproof" => {
5440                    self.advance();
5441                    attrs.leakproof = true;
5442                }
5443                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5444                // spelled-out forms of STRICT and its opposite.
5445                "returns" | "called" => {
5446                    let strict = word == "returns";
5447                    let mut probe = self.pos + 1;
5448                    if strict {
5449                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5450                        // is not ours.
5451                        match self.tokens.get(probe) {
5452                            Some(Token::Null) => probe += 1,
5453                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5454                            _ => return Ok(()),
5455                        }
5456                    }
5457                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5458                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5459                    if !ok {
5460                        return Ok(());
5461                    }
5462                    probe += 1;
5463                    match self.tokens.get(probe) {
5464                        Some(Token::Null) => probe += 1,
5465                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5466                        _ => return Ok(()),
5467                    }
5468                    match self.tokens.get(probe) {
5469                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5470                        _ => return Ok(()),
5471                    }
5472                    self.pos = probe;
5473                    attrs.strict = strict;
5474                }
5475                "security" | "external" => {
5476                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5477                    let mut probe = self.pos + 1;
5478                    if word == "external" {
5479                        match self.tokens.get(probe) {
5480                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5481                                probe += 1;
5482                            }
5483                            _ => return Ok(()),
5484                        }
5485                    }
5486                    let definer = match self.tokens.get(probe) {
5487                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5488                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5489                        _ => return Ok(()),
5490                    };
5491                    self.pos = probe + 1;
5492                    attrs.security_definer = definer;
5493                }
5494                "parallel" => {
5495                    let level = match self.tokens.get(self.pos + 1) {
5496                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5497                            FunctionParallel::Safe
5498                        }
5499                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5500                            FunctionParallel::Restricted
5501                        }
5502                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5503                            FunctionParallel::Unsafe
5504                        }
5505                        _ => return Ok(()),
5506                    };
5507                    self.pos += 2;
5508                    attrs.parallel = level;
5509                }
5510                "cost" | "rows" => {
5511                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5512                        return Ok(());
5513                    };
5514                    self.pos += 2;
5515                    if word == "cost" {
5516                        attrs.cost = Some(n);
5517                    } else {
5518                        attrs.rows = Some(n);
5519                    }
5520                }
5521                _ => return Ok(()),
5522            }
5523        }
5524    }
5525
5526    /// The numeric literal at `idx`, if there is one.
5527    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5528        match self.tokens.get(idx)? {
5529            Token::Integer(n) => Some(*n as f64),
5530            Token::Float(f) => Some(*f),
5531            Token::Numeric(t) => t.parse::<f64>().ok(),
5532            _ => None,
5533        }
5534    }
5535
5536    /// Closing `)`-terminated argument list. v7.12.4 commonly
5537    /// sees the empty `()`; typed args round-trip but the
5538    /// executor (yet) doesn't invoke them.
5539    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5540    /// it away, which is what PG does with one on a function parameter.
5541    fn skip_type_modifier(&mut self) {
5542        if !matches!(self.peek(), Token::LParen) {
5543            return;
5544        }
5545        // Only a numeric modifier — anything else is not one, and eating
5546        // it would swallow real grammar.
5547        let mut i = self.pos + 1;
5548        let mut seen_number = false;
5549        loop {
5550            match self.tokens.get(i) {
5551                Some(Token::Integer(_)) => seen_number = true,
5552                Some(Token::Comma) => {}
5553                Some(Token::RParen) => break,
5554                _ => return,
5555            }
5556            i += 1;
5557        }
5558        if !seen_number {
5559            return;
5560        }
5561        while self.pos <= i {
5562            self.advance();
5563        }
5564    }
5565
5566    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5567        let mut args: Vec<FunctionArg> = Vec::new();
5568        if matches!(self.peek(), Token::RParen) {
5569            self.advance();
5570            return Ok(args);
5571        }
5572        loop {
5573            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5574            // a reserved token; OUT / INOUT are bare idents.
5575            let mode = if matches!(self.peek(), Token::In) {
5576                self.advance();
5577                FunctionArgMode::In
5578            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5579            {
5580                self.advance();
5581                FunctionArgMode::Out
5582            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5583            {
5584                self.advance();
5585                FunctionArgMode::InOut
5586            } else {
5587                FunctionArgMode::In
5588            };
5589            // Optional name. The next token is either a name
5590            // (followed by a type ident) or the type itself.
5591            // Disambiguate by peeking ahead: if the token after
5592            // the next ident is also an ident, we treat the
5593            // first as the name.
5594            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5595            // the comma or paren, then decide. Reading at most two of
5596            // them could not spell `x double precision` at all, and
5597            // silently mis-read the bare `double precision` as a
5598            // parameter named "double" — which is what made the same
5599            // signature key two different ways.
5600            let (name, ty_token) = {
5601                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5602                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5603                    words.push(self.expect_ident_like()?);
5604                }
5605                // v7.39 (round 344) — a length / precision modifier on the
5606                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5607                // accepts it and DROPS it — `pg_get_function_arguments`
5608                // reports plain `character varying` / `numeric`, measured on
5609                // 18.4 — but SPG raised `syntax error at or near "("`,
5610                // because the modifier's parens were never consumed.
5611                self.skip_type_modifier();
5612                // r1049 — `f(v bigint[])`. The array suffix parsed in
5613                // the column position, the cast position and (r1038)
5614                // the RETURNS position, but not here: the fifth
5615                // member of the same family, reported by sentori as
5616                // presumably the same code. It is now.
5617                let array_suffix = self.consume_array_suffix();
5618                let whole = words.join(" ");
5619                let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5620                {
5621                    (Some(words[0].clone()), words[1..].join(" "))
5622                } else {
5623                    (None, whole)
5624                };
5625                ty_token.push_str(&array_suffix);
5626                (name, ty_token)
5627            };
5628            // Type — try to map to ColumnTypeName, else Raw.
5629            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5630                Some(t) => FunctionArgType::Typed(t),
5631                None => FunctionArgType::Raw(ty_token),
5632            };
5633            args.push(FunctionArg { mode, name, ty });
5634            match self.peek() {
5635                Token::Comma => {
5636                    self.advance();
5637                    continue;
5638                }
5639                Token::RParen => {
5640                    self.advance();
5641                    return Ok(args);
5642                }
5643                other => {
5644                    return Err(self.err(alloc::format!(
5645                        "expected , or ) in function arg list, got {other:?}"
5646                    )));
5647                }
5648            }
5649        }
5650    }
5651
5652    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5653        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5654        // function whose row shape is named inline.
5655        if matches!(self.peek(), Token::Table)
5656            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5657        {
5658            self.advance(); // TABLE
5659            self.advance(); // (
5660            let mut cols: Vec<String> = Vec::new();
5661            loop {
5662                let cname = self.expect_ident_like()?;
5663                let mut ty: Vec<String> = Vec::new();
5664                loop {
5665                    match self.peek() {
5666                        Token::Comma | Token::RParen | Token::Eof => break,
5667                        _ => {}
5668                    }
5669                    match self.advance() {
5670                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5671                        other => {
5672                            if let Some(w) = unreserved_keyword_text(&other) {
5673                                ty.push(w);
5674                            }
5675                        }
5676                    }
5677                }
5678                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5679                if matches!(self.peek(), Token::Comma) {
5680                    self.advance();
5681                } else {
5682                    break;
5683                }
5684            }
5685            if matches!(self.peek(), Token::RParen) {
5686                self.advance();
5687            }
5688            return Ok(FunctionReturn::Other(alloc::format!(
5689                "TABLE({})",
5690                cols.join(", ")
5691            )));
5692        }
5693        let ident = self.expect_ident_like()?;
5694        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5695        if ident.eq_ignore_ascii_case("setof") {
5696            let inner = self.expect_ident_like()?;
5697            let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5698            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5699        }
5700        if ident.eq_ignore_ascii_case("trigger") {
5701            return Ok(FunctionReturn::Trigger);
5702        }
5703        if ident.eq_ignore_ascii_case("void") {
5704            return Ok(FunctionReturn::Void);
5705        }
5706        // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5707        // RETURN position did not, so the `[` was a syntax error and the
5708        // whole migration stopped. sentori worked around it by returning
5709        // zero-padded text.
5710        let suffix = self.consume_array_suffix();
5711        if !suffix.is_empty() {
5712            return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5713        }
5714        match map_type_ident_to_column_type_name(&ident) {
5715            Some(t) => Ok(FunctionReturn::Type(t)),
5716            None => Ok(FunctionReturn::Other(ident)),
5717        }
5718    }
5719
5720    /// Consume any `[]` / `[N]` array markers after a type name and give
5721    /// back their text. Empty when there are none.
5722    fn consume_array_suffix(&mut self) -> String {
5723        let mut out = String::new();
5724        while matches!(self.peek(), Token::LBracket) {
5725            self.advance();
5726            // `[N]` is accepted and, as in PG, the length is not enforced.
5727            if let Token::Integer(n) = self.peek().clone() {
5728                self.advance();
5729                out.push_str(&alloc::format!("[{n}]"));
5730            } else {
5731                out.push_str("[]");
5732            }
5733            if matches!(self.peek(), Token::RBracket) {
5734                self.advance();
5735            }
5736        }
5737        out
5738    }
5739
5740    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5741        match self.peek() {
5742            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5743                self.advance();
5744                let lang = self.expect_ident_like()?;
5745                Ok(Some(lang.to_ascii_lowercase()))
5746            }
5747            _ => Ok(None),
5748        }
5749    }
5750
5751    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5752    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5753    /// (expr)]*`. The `DOMAIN` keyword has already been
5754    /// consumed. PG allows the trailing constraints in any
5755    /// order; we approximate with a small loop.
5756    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5757        let name = self.expect_ident_like()?;
5758        // Optional `AS`.
5759        if matches!(self.peek(), Token::As) {
5760            self.advance();
5761        }
5762        // v7.39 (round 259) — keep the raw type NAME when the base is not
5763        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5764        // parent domain.
5765        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _, _, _) =
5766            self.parse_type_with_implied_flags()?;
5767        let mut default: Option<Expr> = None;
5768        let mut not_null = false;
5769        let mut checks: Vec<Expr> = Vec::new();
5770        loop {
5771            match self.peek() {
5772                Token::Default => {
5773                    if default.is_some() {
5774                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5775                    }
5776                    self.advance();
5777                    default = Some(self.parse_expr(0)?);
5778                }
5779                Token::Not => {
5780                    self.advance();
5781                    if !matches!(self.peek(), Token::Null) {
5782                        return Err(self.err(alloc::format!(
5783                            "expected NULL after NOT in DOMAIN, got {:?}",
5784                            self.peek()
5785                        )));
5786                    }
5787                    self.advance();
5788                    not_null = true;
5789                }
5790                Token::Null => {
5791                    self.advance();
5792                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5793                    // is the default-nullable marker (PG accepts it),
5794                    // but AFTER a NOT NULL it is a conflict PG refuses
5795                    // (`conflicting NULL/NOT NULL constraints`,
5796                    // PG18-measured); the old arm no-opped both ways.
5797                    if not_null {
5798                        return Err(self.err(alloc::string::String::from(
5799                            "conflicting NULL/NOT NULL constraints",
5800                        )));
5801                    }
5802                }
5803                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5804                    self.advance();
5805                    if !matches!(self.peek(), Token::LParen) {
5806                        return Err(self.err(alloc::format!(
5807                            "expected '(' after CHECK in DOMAIN, got {:?}",
5808                            self.peek()
5809                        )));
5810                    }
5811                    self.advance();
5812                    let expr = self.parse_expr(0)?;
5813                    if !matches!(self.peek(), Token::RParen) {
5814                        return Err(self.err(alloc::format!(
5815                            "expected ')' after CHECK expr, got {:?}",
5816                            self.peek()
5817                        )));
5818                    }
5819                    self.advance();
5820                    checks.push(expr);
5821                }
5822                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5823                // prefix on the constraint; we drop the name and
5824                // recurse into the constraint parsing.
5825                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5826                    self.advance();
5827                    let _ = self.expect_ident_like()?;
5828                }
5829                _ => break,
5830            }
5831        }
5832        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5833            name,
5834            base_type,
5835            base_domain: base_user_ref,
5836            default,
5837            not_null,
5838            checks,
5839        }))
5840    }
5841
5842    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5843    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5844    /// consumed.
5845    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5846        let name = self.expect_ident_like()?;
5847        // Required `AS`.
5848        if !matches!(self.peek(), Token::As) {
5849            return Err(self.err(alloc::format!(
5850                "expected AS after CREATE TYPE {name:?}, got {:?}",
5851                self.peek()
5852            )));
5853        }
5854        self.advance();
5855        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5856        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5857        // on the next token: `(` = composite, ident `ENUM` = enum.
5858        if matches!(self.peek(), Token::LParen) {
5859            self.advance();
5860            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5861            let mut field_user_types: Vec<Option<String>> = Vec::new();
5862            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5863            // is legal PG (an attribute-less composite; measured — the old
5864            // e2e note claimed PG requires at least one attribute).
5865            if matches!(self.peek(), Token::RParen) {
5866                self.advance();
5867                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5868                    name,
5869                    kind: crate::ast::TypeKind::Composite {
5870                        fields,
5871                        field_user_types,
5872                    },
5873                }));
5874            }
5875            loop {
5876                let field_name = self.expect_ident_like()?;
5877                // v7.39 (round 264) — keep the raw type name when it is not
5878                // a builtin: that is how a NESTED composite field records
5879                // which composite it holds.
5880                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _, _, _) =
5881                    self.parse_type_with_implied_flags()?;
5882                fields.push((field_name, field_type));
5883                field_user_types.push(field_user_ref);
5884                if matches!(self.peek(), Token::Comma) {
5885                    self.advance();
5886                    continue;
5887                }
5888                if matches!(self.peek(), Token::RParen) {
5889                    self.advance();
5890                    break;
5891                }
5892                return Err(self.err(alloc::format!(
5893                    "expected , or ) in composite field list, got {:?}",
5894                    self.peek()
5895                )));
5896            }
5897            if fields.is_empty() {
5898                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5899            }
5900            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5901                name,
5902                kind: crate::ast::TypeKind::Composite {
5903                    fields,
5904                    field_user_types,
5905                },
5906            }));
5907        }
5908        // Required `ENUM` ident.
5909        let kind_ident = match self.peek().clone() {
5910            Token::Ident(s) | Token::QuotedIdent(s) => s,
5911            other => {
5912                return Err(self.err(alloc::format!(
5913                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5914                )));
5915            }
5916        };
5917        if !kind_ident.eq_ignore_ascii_case("enum") {
5918            return Err(self.err(alloc::format!(
5919                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5920            )));
5921        }
5922        self.advance();
5923        if !matches!(self.peek(), Token::LParen) {
5924            return Err(self.err(alloc::format!(
5925                "expected '(' after ENUM, got {:?}",
5926                self.peek()
5927            )));
5928        }
5929        self.advance();
5930        let mut labels: Vec<String> = Vec::new();
5931        loop {
5932            match self.peek().clone() {
5933                Token::String(s) => {
5934                    self.advance();
5935                    labels.push(s);
5936                }
5937                other => {
5938                    return Err(
5939                        self.err(alloc::format!("expected enum label string, got {other:?}"))
5940                    );
5941                }
5942            }
5943            if matches!(self.peek(), Token::Comma) {
5944                self.advance();
5945                continue;
5946            }
5947            if matches!(self.peek(), Token::RParen) {
5948                self.advance();
5949                break;
5950            }
5951            return Err(self.err(alloc::format!(
5952                "expected , or ) in ENUM label list, got {:?}",
5953                self.peek()
5954            )));
5955        }
5956        if labels.is_empty() {
5957            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5958        }
5959        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5960            name,
5961            kind: crate::ast::TypeKind::Enum { labels },
5962        }))
5963    }
5964
5965    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5966    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5967    /// The `CREATE MATERIALIZED VIEW` keywords have already been
5968    /// consumed.
5969    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5970        let if_not_exists = self.parse_if_not_exists();
5971        let name = self.expect_ident_like()?;
5972        let mut columns: Vec<String> = Vec::new();
5973        if matches!(self.peek(), Token::LParen) {
5974            self.advance();
5975            loop {
5976                let c = self.expect_ident_like()?;
5977                columns.push(c);
5978                if matches!(self.peek(), Token::Comma) {
5979                    self.advance();
5980                    continue;
5981                }
5982                if matches!(self.peek(), Token::RParen) {
5983                    self.advance();
5984                    break;
5985                }
5986                return Err(self.err(alloc::format!(
5987                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5988                    self.peek()
5989                )));
5990            }
5991        }
5992        if !matches!(self.peek(), Token::As) {
5993            return Err(self.err(alloc::format!(
5994                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5995                self.peek()
5996            )));
5997        }
5998        self.advance();
5999        // v7.39 (round 151) — a WITH-headed body is legal (read-only
6000        // CTEs only; the engine rejects data-modifying ones with PG's
6001        // message). A trailing `WITH [NO] DATA` can't START the body,
6002        // so WITH here heads the query.
6003        let body = if self.peek_is_with_kw() {
6004            self.advance();
6005            self.parse_nested_with_select()?
6006        } else {
6007            let body_stmt = self.parse_select_stmt()?;
6008            let Statement::Select(body) = body_stmt else {
6009                return Err(self.err(alloc::format!(
6010                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
6011                )));
6012            };
6013            body
6014        };
6015        // Optional trailing `WITH [NO] DATA`.
6016        let with_data = self.parse_optional_with_data(true)?;
6017        Ok(Statement::CreateMaterializedView(
6018            crate::ast::CreateMaterializedViewStatement {
6019                temporary: false,
6020                name,
6021                if_not_exists,
6022                columns,
6023                body,
6024                with_data,
6025                as_plain_table: false,
6026            },
6027        ))
6028    }
6029
6030    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
6031    /// `default_when_absent` is what to return if the tail is
6032    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
6033    /// WITH DATA).
6034    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
6035        let save = self.pos;
6036        // `WITH` is an Ident (not reserved in the lexer).
6037        let is_with = match self.peek() {
6038            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
6039            _ => false,
6040        };
6041        if !is_with {
6042            return Ok(default_when_absent);
6043        }
6044        self.advance();
6045        // Optional `NO`.
6046        let mut with_data = true;
6047        let is_no = match self.peek() {
6048            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
6049            _ => false,
6050        };
6051        if is_no {
6052            self.advance();
6053            with_data = false;
6054        }
6055        // Required `DATA` ident.
6056        let is_data = match self.peek() {
6057            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
6058            _ => false,
6059        };
6060        if is_data {
6061            self.advance();
6062            Ok(with_data)
6063        } else {
6064            // Caller's WITH wasn't WITH-DATA — rewind so the outer
6065            // parser can interpret it.
6066            self.pos = save;
6067            Ok(default_when_absent)
6068        }
6069    }
6070
6071    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
6072    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
6073    /// All keyword prefixes have already been consumed; the flags
6074    /// say which were present.
6075    fn parse_create_view_after_keyword(
6076        &mut self,
6077        or_replace: bool,
6078        _materialized_unused: bool,
6079        temporary: bool,
6080    ) -> Result<Statement, ParseError> {
6081        let if_not_exists = self.parse_if_not_exists();
6082        let name = self.expect_ident_like()?;
6083        // Optional `(col, col, …)` rename list.
6084        let mut columns: Vec<String> = Vec::new();
6085        if matches!(self.peek(), Token::LParen) {
6086            self.advance();
6087            loop {
6088                let c = self.expect_ident_like()?;
6089                columns.push(c);
6090                if matches!(self.peek(), Token::Comma) {
6091                    self.advance();
6092                    continue;
6093                }
6094                if matches!(self.peek(), Token::RParen) {
6095                    self.advance();
6096                    break;
6097                }
6098                return Err(self.err(alloc::format!(
6099                    "expected , or ) in VIEW column list, got {:?}",
6100                    self.peek()
6101                )));
6102            }
6103        }
6104        // Required `AS`.
6105        if !matches!(self.peek(), Token::As) {
6106            return Err(self.err(alloc::format!(
6107                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
6108                self.peek()
6109            )));
6110        }
6111        self.advance();
6112        // Body: a regular SELECT statement. v7.39 (round 151) — a
6113        // WITH-headed body is legal too (read-only CTEs only; the
6114        // engine rejects data-modifying ones with PG's message).
6115        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
6116        // with the check-option clause, so WITH here heads the query.
6117        let body = if self.peek_is_with_kw() {
6118            self.advance();
6119            self.parse_nested_with_select()?
6120        } else {
6121            let body_stmt = self.parse_select_stmt()?;
6122            let Statement::Select(body) = body_stmt else {
6123                return Err(self.err(alloc::format!(
6124                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
6125                )));
6126            };
6127            body
6128        };
6129        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
6130        // The SELECT parser stops before a trailing WITH, so it lands here.
6131        let check_option = if matches!(self.peek(),
6132            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
6133        {
6134            self.advance(); // WITH
6135            let opt = match self.peek() {
6136                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6137                    self.advance();
6138                    crate::ast::ViewCheckOption::Local
6139                }
6140                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6141                    self.advance();
6142                    crate::ast::ViewCheckOption::Cascaded
6143                }
6144                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6145                _ => crate::ast::ViewCheckOption::Cascaded,
6146            };
6147            if !matches!(self.peek(),
6148                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6149            {
6150                return Err(self.err(alloc::format!(
6151                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6152                    self.peek()
6153                )));
6154            }
6155            self.advance(); // CHECK
6156            if !matches!(self.peek(),
6157                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6158            {
6159                return Err(self.err(alloc::format!(
6160                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6161                    self.peek()
6162                )));
6163            }
6164            self.advance(); // OPTION
6165            Some(opt)
6166        } else {
6167            None
6168        };
6169        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6170            name,
6171            or_replace,
6172            if_not_exists,
6173            temporary,
6174            columns,
6175            body,
6176            check_option,
6177        }))
6178    }
6179
6180    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6181    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6182    /// consumed; `temporary` carries whether TEMPORARY was seen.
6183    fn parse_create_sequence_after_keyword(
6184        &mut self,
6185        temporary: bool,
6186    ) -> Result<Statement, ParseError> {
6187        let if_not_exists = self.parse_if_not_exists();
6188        let name = self.expect_ident_like()?;
6189        // Optional `AS data_type`.
6190        let data_type = if matches!(self.peek(), Token::As) {
6191            self.advance();
6192            Some(self.parse_sequence_data_type()?)
6193        } else {
6194            None
6195        };
6196        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6197        Ok(Statement::CreateSequence(
6198            crate::ast::CreateSequenceStatement {
6199                name,
6200                if_not_exists,
6201                temporary,
6202                data_type,
6203                options,
6204            },
6205        ))
6206    }
6207
6208    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6209    /// already been consumed; this is reached after `SEQUENCE`.
6210    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6211    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6212        use crate::ast::AlterDomainAction as A;
6213        let name = self.expect_ident_like()?;
6214        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6215        let kw = match self.peek() {
6216            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6217            Token::Drop => alloc::string::String::from("drop"),
6218            Token::Default => alloc::string::String::from("default"),
6219            other => {
6220                return Err(self.err(alloc::format!(
6221                    "expected an ALTER DOMAIN action, got {other:?}"
6222                )));
6223            }
6224        };
6225        let action = match kw.as_str() {
6226            "add" => {
6227                self.advance();
6228                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6229                {
6230                    self.advance();
6231                    Some(self.expect_ident_like()?)
6232                } else {
6233                    None
6234                };
6235                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6236                    return Err(self.err(alloc::format!(
6237                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6238                        self.peek()
6239                    )));
6240                }
6241                self.advance();
6242                if !matches!(self.peek(), Token::LParen) {
6243                    return Err(self.err("expected '(' after CHECK".into()));
6244                }
6245                self.advance();
6246                let check = self.parse_expr(0)?;
6247                if !matches!(self.peek(), Token::RParen) {
6248                    return Err(self.err("expected ')' after CHECK expression".into()));
6249                }
6250                self.advance();
6251                A::AddConstraint { name: cname, check }
6252            }
6253            "drop" => {
6254                self.advance();
6255                match self.peek() {
6256                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6257                        self.advance();
6258                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6259                        {
6260                            self.advance();
6261                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6262                            {
6263                                return Err(self.err("expected EXISTS after IF".into()));
6264                            }
6265                            self.advance();
6266                            true
6267                        } else {
6268                            false
6269                        };
6270                        let cn = self.expect_ident_like()?;
6271                        A::DropConstraint {
6272                            name: cn,
6273                            if_exists,
6274                        }
6275                    }
6276                    Token::Default => {
6277                        self.advance();
6278                        A::DropDefault
6279                    }
6280                    Token::Not => {
6281                        self.advance();
6282                        if !matches!(self.peek(), Token::Null) {
6283                            return Err(self.err("expected NULL after NOT".into()));
6284                        }
6285                        self.advance();
6286                        A::DropNotNull
6287                    }
6288                    other => {
6289                        return Err(self.err(alloc::format!(
6290                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6291                        )));
6292                    }
6293                }
6294            }
6295            "set" => {
6296                self.advance();
6297                match self.peek() {
6298                    Token::Default => {
6299                        self.advance();
6300                        A::SetDefault(self.parse_expr(0)?)
6301                    }
6302                    Token::Not => {
6303                        self.advance();
6304                        if !matches!(self.peek(), Token::Null) {
6305                            return Err(self.err("expected NULL after NOT".into()));
6306                        }
6307                        self.advance();
6308                        A::SetNotNull
6309                    }
6310                    other => {
6311                        return Err(self.err(alloc::format!(
6312                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6313                        )));
6314                    }
6315                }
6316            }
6317            "rename" => {
6318                self.advance();
6319                if !matches!(self.peek(), Token::To) {
6320                    return Err(self.err("expected TO after RENAME".into()));
6321                }
6322                self.advance();
6323                A::RenameTo(self.expect_ident_like()?)
6324            }
6325            other => {
6326                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6327            }
6328        };
6329        Ok(Statement::AlterDomain { name, action })
6330    }
6331
6332    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6333        let if_exists = self.parse_if_exists();
6334        let name = self.expect_ident_like()?;
6335        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6336        // the option list (PG allows only one or the other).
6337        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6338            self.advance();
6339            if matches!(self.peek(), Token::To) {
6340                self.advance();
6341            } else {
6342                self.expect_keyword_ident("to")?;
6343            }
6344            let new = self.expect_ident_like()?;
6345            return Ok(Statement::AlterSequence(
6346                crate::ast::AlterSequenceStatement {
6347                    name,
6348                    if_exists,
6349                    options: crate::ast::SequenceOptions::default(),
6350                    rename_to: Some(new),
6351                },
6352            ));
6353        }
6354        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6355        Ok(Statement::AlterSequence(
6356            crate::ast::AlterSequenceStatement {
6357                name,
6358                if_exists,
6359                options,
6360                rename_to: None,
6361            },
6362        ))
6363    }
6364
6365    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6366        let kw = self.expect_ident_like()?;
6367        match kw.to_ascii_lowercase().as_str() {
6368            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6369            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6370            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6371            other => Err(self.err(alloc::format!(
6372                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6373            ))),
6374        }
6375    }
6376
6377    fn parse_sequence_options(
6378        &mut self,
6379        allow_restart: bool,
6380    ) -> Result<crate::ast::SequenceOptions, ParseError> {
6381        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6382        let mut opts = SequenceOptions::default();
6383        #[allow(clippy::while_let_loop)]
6384        loop {
6385            // Match an ident; stop at any non-ident token (sentinel,
6386            // semicolon, end of statement).
6387            let kw_lc = match self.peek() {
6388                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6389                _ => break,
6390            };
6391            match kw_lc.as_str() {
6392                "increment" => {
6393                    self.advance();
6394                    // Optional BY.
6395                    if self.peek_is_by() {
6396                        self.advance();
6397                    }
6398                    opts.increment = Some(self.expect_signed_int()?);
6399                }
6400                "minvalue" => {
6401                    self.advance();
6402                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6403                }
6404                "maxvalue" => {
6405                    self.advance();
6406                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6407                }
6408                "no" => {
6409                    self.advance();
6410                    let what = self.expect_ident_like()?;
6411                    match what.to_ascii_lowercase().as_str() {
6412                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6413                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6414                        "cycle" => opts.cycle = Some(false),
6415                        other => {
6416                            return Err(self.err(alloc::format!(
6417                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6418                            )));
6419                        }
6420                    }
6421                }
6422                "start" => {
6423                    self.advance();
6424                    // Optional WITH.
6425                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6426                        if s.eq_ignore_ascii_case("with"))
6427                    {
6428                        self.advance();
6429                    }
6430                    opts.start = Some(self.expect_signed_int()?);
6431                }
6432                "restart" if allow_restart => {
6433                    self.advance();
6434                    // Optional WITH n; bare RESTART means restart at START.
6435                    let mut with_val: Option<i64> = None;
6436                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6437                        if s.eq_ignore_ascii_case("with"))
6438                    {
6439                        self.advance();
6440                        with_val = Some(self.expect_signed_int()?);
6441                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6442                        with_val = Some(self.expect_signed_int()?);
6443                    }
6444                    opts.restart = Some(with_val);
6445                }
6446                "cache" => {
6447                    self.advance();
6448                    opts.cache = Some(self.expect_signed_int()?);
6449                }
6450                "cycle" => {
6451                    self.advance();
6452                    opts.cycle = Some(true);
6453                }
6454                "owned" => {
6455                    self.advance();
6456                    match self.peek() {
6457                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6458                            self.advance();
6459                        }
6460                        other => {
6461                            return Err(
6462                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6463                            );
6464                        }
6465                    }
6466                    // OWNED BY {NONE | tab.col}. Read just one ident
6467                    // (NOT expect_ident_like which would auto-strip
6468                    // a schema prefix and consume the `.col` we need).
6469                    let first = match self.advance() {
6470                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6471                        other => {
6472                            return Err(self.err(alloc::format!(
6473                                "expected identifier or NONE after OWNED BY, got {other:?}"
6474                            )));
6475                        }
6476                    };
6477                    if first.eq_ignore_ascii_case("none") {
6478                        opts.owned_by = Some(SequenceOwnedBy::None);
6479                    } else if matches!(self.peek(), Token::Dot) {
6480                        self.advance();
6481                        let second = match self.advance() {
6482                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6483                            other => {
6484                                return Err(self.err(alloc::format!(
6485                                    "expected column name after OWNED BY {first}., got {other:?}"
6486                                )));
6487                            }
6488                        };
6489                        // v7.17 dump-compat fix — pg_dump emits
6490                        // OWNED BY clauses as
6491                        // `schema.table.column` (three segments).
6492                        // If a third `.<ident>` follows, treat the
6493                        // first ident as schema (drop it; SPG is
6494                        // single-schema) and the middle / last
6495                        // pair as table.column. Otherwise it's
6496                        // the two-segment form table.column.
6497                        if matches!(self.peek(), Token::Dot) {
6498                            self.advance();
6499                            let third = match self.advance() {
6500                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6501                                other => {
6502                                    return Err(self.err(alloc::format!(
6503                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6504                                    )));
6505                                }
6506                            };
6507                            let _ = first; // schema prefix discarded
6508                            opts.owned_by = Some(SequenceOwnedBy::Column {
6509                                table: second,
6510                                column: third,
6511                            });
6512                        } else {
6513                            opts.owned_by = Some(SequenceOwnedBy::Column {
6514                                table: first,
6515                                column: second,
6516                            });
6517                        }
6518                    } else {
6519                        return Err(self.err(alloc::format!(
6520                            "expected table.column or NONE after OWNED BY, got {first:?}"
6521                        )));
6522                    }
6523                }
6524                _ => break,
6525            }
6526        }
6527        Ok(opts)
6528    }
6529
6530    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6531        let neg = if matches!(self.peek(), Token::Minus) {
6532            self.advance();
6533            true
6534        } else {
6535            false
6536        };
6537        match self.peek() {
6538            Token::Integer(n) => {
6539                let v = *n;
6540                self.advance();
6541                Ok(if neg { -v } else { v })
6542            }
6543            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6544        }
6545    }
6546
6547    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6548    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6549    /// clause is fully accepted and discarded — SPG always runs
6550    /// constraint checks immediately (single-writer model). The
6551    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6552    /// in either order (per the SQL spec they're independent),
6553    /// though pg_dump always emits them in the canonical
6554    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6555    /// Stops at the first token that isn't part of the clause.
6556    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6557        self.consume_deferrable_clauses_timed().map(|_| ())
6558    }
6559
6560    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6561    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6562    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6563    /// NOT DEFERRABLE and a circular-FK migration could not load.
6564    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6565        let mut deferrable = false;
6566        let mut initially_deferred = false;
6567        loop {
6568            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6569            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6570                self.advance();
6571                deferrable = true;
6572                if self.consume_optional_initially_clause()? {
6573                    initially_deferred = true;
6574                }
6575                continue;
6576            }
6577            // `NOT DEFERRABLE` — already worked pre-3.1.
6578            if matches!(self.peek(), Token::Not) {
6579                let look = self.tokens.get(self.pos + 1);
6580                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6581                    self.advance(); // NOT
6582                    self.advance(); // DEFERRABLE
6583                    deferrable = false;
6584                    initially_deferred = false;
6585                    let _ = self.consume_optional_initially_clause()?;
6586                    continue;
6587                }
6588                break;
6589            }
6590            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6591            // accepts this without a leading [NOT] DEFERRABLE
6592            // (the timing keyword alone). pg_dump occasionally
6593            // emits it on FK constraints that inherit timing.
6594            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6595                if self.consume_optional_initially_clause()? {
6596                    initially_deferred = true;
6597                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6598                    deferrable = true;
6599                }
6600                continue;
6601            }
6602            break;
6603        }
6604        Ok((deferrable, initially_deferred))
6605    }
6606
6607    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6608    /// next token is `INITIALLY`, consume it plus the required
6609    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6610    /// Returns true when the timing seen was `DEFERRED`.
6611    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6612        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6613            return Ok(false);
6614        }
6615        self.advance(); // INITIALLY
6616        match self.advance() {
6617            Token::Ident(s)
6618                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6619            {
6620                Ok(s.eq_ignore_ascii_case("deferred"))
6621            }
6622            other => Err(self.err(alloc::format!(
6623                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6624            ))),
6625        }
6626    }
6627
6628    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6629    /// in its entirety so the parser returns Empty without
6630    /// touching the runtime. The CREATE+PROCEDURE keywords are
6631    /// already consumed; this swallows everything from the
6632    /// procedure name through the matching `END`, including
6633    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6634    /// (DELIMITER `//` makes the script splitter forward the
6635    /// whole block as one statement), `@var` session-variable
6636    /// references, and the trailing terminator.
6637    ///
6638    /// Tracks nesting depth so:
6639    ///   BEGIN
6640    ///     IF cond THEN
6641    ///       BEGIN ... END;
6642    ///     END IF;
6643    ///   END
6644    /// terminates at the outer END.
6645    fn consume_mysql_routine_body(&mut self) {
6646        // Outer skeleton: name, (...), optional clauses, BEGIN
6647        // <body> END [;]. Scan for the first BEGIN — anything
6648        // before it is signature decoration we don't care about.
6649        // Once inside BEGIN, count up on BEGIN, down on END.
6650        let mut depth: i32 = 0;
6651        let mut started = false;
6652        loop {
6653            match self.peek().clone() {
6654                Token::Begin => {
6655                    self.advance();
6656                    depth += 1;
6657                    started = true;
6658                }
6659                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6660                    self.advance();
6661                    if started {
6662                        depth -= 1;
6663                        if depth <= 0 {
6664                            // Optional trailing ident (`END IF`,
6665                            // `END LOOP`, `END WHILE`, `END CASE`,
6666                            // `END label_name`) — eat the next
6667                            // ident if present so we don't
6668                            // mistake `END IF;` for the outer
6669                            // close.
6670                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6671                                // If the next token is one of the
6672                                // PL/SQL block-closer keywords,
6673                                // the END belongs to an inner
6674                                // block; bump depth back up.
6675                                let is_inner_close = matches!(
6676                                    self.peek(),
6677                                    Token::Ident(s) | Token::QuotedIdent(s)
6678                                        if matches!(
6679                                            s.to_ascii_lowercase().as_str(),
6680                                            "if" | "loop" | "while" | "case" | "repeat"
6681                                        )
6682                                );
6683                                if is_inner_close {
6684                                    self.advance();
6685                                    depth += 1;
6686                                    continue;
6687                                }
6688                            }
6689                            // Eat optional trailing `;`.
6690                            if matches!(self.peek(), Token::Semicolon) {
6691                                self.advance();
6692                            }
6693                            return;
6694                        }
6695                    }
6696                }
6697                Token::Eof => return,
6698                _ => {
6699                    self.advance();
6700                }
6701            }
6702        }
6703    }
6704
6705    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6706    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6707    ///
6708    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6709    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6710    ///   ident, or `ident @ ident-or-quoted-string` host form)
6711    /// * `SQL SECURITY {DEFINER|INVOKER}`
6712    ///
6713    /// Each clause may appear at most once but in any order.
6714    /// The hints are pure planner / permission metadata that
6715    /// SPG's view-rewrite engine handles uniformly; we accept
6716    /// and discard. Returns `Ok(())` once a non-clause token is
6717    /// peeked (the caller then checks for the `VIEW` keyword).
6718    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6719        loop {
6720            match self.peek().clone() {
6721                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6722                    self.advance(); // ALGORITHM
6723                    // Optional `=`. MySQL spec requires it but be
6724                    // generous.
6725                    if matches!(self.peek(), Token::Eq) {
6726                        self.advance();
6727                    }
6728                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6729                    // bare ident; unknown values still parse so
6730                    // future MySQL versions don't break.
6731                    if matches!(
6732                        self.peek(),
6733                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6734                    ) {
6735                        self.advance();
6736                    }
6737                }
6738                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6739                    self.advance(); // DEFINER
6740                    if matches!(self.peek(), Token::Eq) {
6741                        self.advance();
6742                    }
6743                    // User: quoted string, ident, OR ident @ host
6744                    // (host may itself be quoted or bare).
6745                    match self.peek().clone() {
6746                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6747                            self.advance();
6748                            // Optional `@host`.
6749                            if matches!(self.peek(), Token::At) {
6750                                self.advance();
6751                                if matches!(
6752                                    self.peek(),
6753                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6754                                ) {
6755                                    self.advance();
6756                                }
6757                            }
6758                        }
6759                        _ => {}
6760                    }
6761                }
6762                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6763                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6764                    // when followed by SECURITY — the dispatcher must
6765                    // not consume a bare `SQL` token (it's not a
6766                    // legal CREATE prefix on its own).
6767                    let save = self.pos;
6768                    self.advance(); // SQL
6769                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6770                        if s2.eq_ignore_ascii_case("security"))
6771                    {
6772                        self.advance(); // SECURITY
6773                        // DEFINER / INVOKER trailing ident.
6774                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6775                            self.advance();
6776                        }
6777                    } else {
6778                        // Not a SQL SECURITY clause — roll back and
6779                        // bail; the caller will error out cleanly.
6780                        self.pos = save;
6781                        return Ok(());
6782                    }
6783                }
6784                _ => return Ok(()),
6785            }
6786        }
6787    }
6788
6789    fn parse_if_not_exists(&mut self) -> bool {
6790        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6791        {
6792            let save = self.pos;
6793            self.advance();
6794            if matches!(self.peek(), Token::Not) {
6795                self.advance();
6796                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6797                {
6798                    self.advance();
6799                    return true;
6800                }
6801            }
6802            self.pos = save;
6803        }
6804        false
6805    }
6806
6807    fn parse_if_exists(&mut self) -> bool {
6808        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6809        {
6810            let save = self.pos;
6811            self.advance();
6812            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6813            {
6814                self.advance();
6815                return true;
6816            }
6817            self.pos = save;
6818        }
6819        false
6820    }
6821
6822    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6823    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6824    /// been consumed.
6825    fn parse_create_trigger_after_keyword(
6826        &mut self,
6827        or_replace: bool,
6828    ) -> Result<Statement, ParseError> {
6829        let name = self.expect_ident_like()?;
6830        let timing = {
6831            let ident = self.expect_ident_like()?;
6832            if ident.eq_ignore_ascii_case("before") {
6833                TriggerTiming::Before
6834            } else if ident.eq_ignore_ascii_case("after") {
6835                TriggerTiming::After
6836            } else if ident.eq_ignore_ascii_case("instead") {
6837                let next = self.expect_ident_like()?;
6838                if !next.eq_ignore_ascii_case("of") {
6839                    return Err(self.err(alloc::format!(
6840                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6841                    )));
6842                }
6843                TriggerTiming::InsteadOf
6844            } else {
6845                return Err(self.err(alloc::format!(
6846                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6847                )));
6848            }
6849        };
6850        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6851        // OR is a reserved keyword token (Token::Or), not an Ident.
6852        // v7.13.0 — after an UPDATE event we may optionally see
6853        // `OF col, col, …` (mailrs round-5 G7). Columns are
6854        // captured into `update_columns` once across the whole
6855        // events list; multiple `UPDATE OF` clauses are rejected.
6856        let mut events: Vec<TriggerEvent> = Vec::new();
6857        let mut update_columns: Vec<String> = Vec::new();
6858        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6859        events.push(first_ev);
6860        if !first_cols.is_empty() {
6861            update_columns = first_cols;
6862        }
6863        while matches!(self.peek(), Token::Or) {
6864            self.advance();
6865            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6866            events.push(ev);
6867            if !cols.is_empty() {
6868                if !update_columns.is_empty() {
6869                    return Err(
6870                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6871                    );
6872                }
6873                update_columns = cols;
6874            }
6875        }
6876        // ON <table>
6877        let tok = self.peek();
6878        let Token::On = tok else {
6879            return Err(self.err(alloc::format!(
6880                "expected ON after trigger events, got {tok:?}"
6881            )));
6882        };
6883        self.advance();
6884        let table = self.expect_ident_like()?;
6885        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6886        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6887        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6888        // the trigger as a plain AFTER trigger (correct for every non-deferred
6889        // use; deferral timing is not yet honoured).
6890        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6891            if s.eq_ignore_ascii_case("from"))
6892        {
6893            self.advance();
6894            let _reftable = self.expect_ident_like()?;
6895        }
6896        self.consume_optional_deferrable_clauses()?;
6897        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6898        // keyword (Token::For); EACH / ROW / STATEMENT are bare
6899        // idents.
6900        if !matches!(self.peek(), Token::For) {
6901            return Err(self.err(alloc::format!(
6902                "expected FOR EACH ROW / STATEMENT, got {:?}",
6903                self.peek()
6904            )));
6905        }
6906        self.advance();
6907        let for_each = {
6908            let e = self.expect_ident_like()?;
6909            if !e.eq_ignore_ascii_case("each") {
6910                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6911            }
6912            let unit = self.expect_ident_like()?;
6913            if unit.eq_ignore_ascii_case("row") {
6914                TriggerForEach::Row
6915            } else if unit.eq_ignore_ascii_case("statement") {
6916                TriggerForEach::Statement
6917            } else {
6918                return Err(self.err(alloc::format!(
6919                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6920                )));
6921            }
6922        };
6923        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6924        let when_condition = if matches!(self.peek(),
6925            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6926        {
6927            self.advance();
6928            Some(self.parse_paren_expr("WHEN")?)
6929        } else {
6930            None
6931        };
6932        // EXECUTE FUNCTION/PROCEDURE name(...)
6933        let exec = self.expect_ident_like()?;
6934        if !exec.eq_ignore_ascii_case("execute") {
6935            return Err(self.err(alloc::format!(
6936                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6937            )));
6938        }
6939        let fn_or_proc = self.expect_ident_like()?;
6940        if !(fn_or_proc.eq_ignore_ascii_case("function")
6941            || fn_or_proc.eq_ignore_ascii_case("procedure"))
6942        {
6943            return Err(self.err(alloc::format!(
6944                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6945            )));
6946        }
6947        let function = self.expect_ident_like()?;
6948        // Optional empty arg list `()`.
6949        if matches!(self.peek(), Token::LParen) {
6950            self.advance();
6951            if !matches!(self.peek(), Token::RParen) {
6952                return Err(self.err(alloc::format!(
6953                    "v7.12.4 trigger function calls take no args; got {:?}",
6954                    self.peek()
6955                )));
6956            }
6957            self.advance();
6958        }
6959        Ok(Statement::CreateTrigger(CreateTriggerStatement {
6960            name,
6961            or_replace,
6962            timing,
6963            events,
6964            table,
6965            for_each,
6966            function,
6967            update_columns,
6968            when_condition,
6969        }))
6970    }
6971
6972    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6973    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6974    fn parse_create_rule_after_keyword(
6975        &mut self,
6976        or_replace: bool,
6977    ) -> Result<Statement, ParseError> {
6978        let name = self.expect_ident_like()?;
6979        if !matches!(self.peek(), Token::As) {
6980            return Err(self.err(alloc::format!(
6981                "expected AS in CREATE RULE, got {:?}",
6982                self.peek()
6983            )));
6984        }
6985        self.advance();
6986        if !matches!(self.peek(), Token::On) {
6987            return Err(self.err(alloc::format!(
6988                "expected ON in CREATE RULE, got {:?}",
6989                self.peek()
6990            )));
6991        }
6992        self.advance();
6993        let event = self.parse_rule_event()?;
6994        if !matches!(self.peek(), Token::To)
6995            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6996        {
6997            return Err(self.err(alloc::format!(
6998                "expected TO after rule event, got {:?}",
6999                self.peek()
7000            )));
7001        }
7002        self.advance();
7003        let table = self.expect_ident_like()?;
7004        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
7005        let when_condition = if matches!(self.peek(), Token::Where) {
7006            self.advance();
7007            Some(self.parse_expr(0)?)
7008        } else {
7009            None
7010        };
7011        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
7012        {
7013            return Err(self.err(alloc::format!(
7014                "expected DO in CREATE RULE, got {:?}",
7015                self.peek()
7016            )));
7017        }
7018        self.advance();
7019        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
7020        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
7021        {
7022            self.advance();
7023            true
7024        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
7025            self.advance();
7026            false
7027        } else {
7028            false
7029        };
7030        // `NOTHING` | `( cmd; … )` | `cmd`.
7031        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
7032        {
7033            self.advance();
7034            Vec::new()
7035        } else if matches!(self.peek(), Token::LParen) {
7036            self.advance();
7037            let mut cmds = Vec::new();
7038            loop {
7039                cmds.push(self.parse_one_statement()?);
7040                if matches!(self.peek(), Token::Semicolon) {
7041                    self.advance();
7042                    if matches!(self.peek(), Token::RParen) {
7043                        break;
7044                    }
7045                    continue;
7046                }
7047                break;
7048            }
7049            if !matches!(self.peek(), Token::RParen) {
7050                return Err(self.err(alloc::format!(
7051                    "expected ) closing the CREATE RULE command list, got {:?}",
7052                    self.peek()
7053                )));
7054            }
7055            self.advance();
7056            cmds
7057        } else {
7058            alloc::vec![self.parse_one_statement()?]
7059        };
7060        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
7061            name,
7062            or_replace,
7063            event,
7064            table,
7065            instead,
7066            when_condition,
7067            commands,
7068        }))
7069    }
7070
7071    /// v7.39 (round 139) — a rule event keyword → uppercase string.
7072    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
7073        if matches!(self.peek(), Token::Insert) {
7074            self.advance();
7075            return Ok(alloc::string::String::from("INSERT"));
7076        }
7077        if matches!(self.peek(), Token::Select) {
7078            self.advance();
7079            return Ok(alloc::string::String::from("SELECT"));
7080        }
7081        match self.peek() {
7082            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7083                self.advance();
7084                Ok(alloc::string::String::from("UPDATE"))
7085            }
7086            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7087                self.advance();
7088                Ok(alloc::string::String::from("DELETE"))
7089            }
7090            other => Err(self.err(alloc::format!(
7091                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
7092            ))),
7093        }
7094    }
7095
7096    /// v7.13.0 — parse one trigger event, then optionally consume
7097    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
7098    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
7099    fn parse_trigger_event_with_optional_of(
7100        &mut self,
7101    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
7102        let ev = self.parse_trigger_event()?;
7103        if !matches!(ev, TriggerEvent::Update) {
7104            return Ok((ev, Vec::new()));
7105        }
7106        // `OF` is a bare ident.
7107        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
7108            return Ok((ev, Vec::new()));
7109        }
7110        self.advance(); // OF
7111        let mut cols: Vec<String> = Vec::new();
7112        loop {
7113            cols.push(self.expect_ident_like()?);
7114            if matches!(self.peek(), Token::Comma) {
7115                self.advance();
7116                continue;
7117            }
7118            break;
7119        }
7120        if cols.is_empty() {
7121            return Err(
7122                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
7123            );
7124        }
7125        Ok((ev, cols))
7126    }
7127
7128    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
7129    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
7130    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
7131    /// inside the body.
7132    /// Called by [`parse_plpgsql_body`] after the body's tokens
7133    /// have been lexed into this temporary parser.
7134    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7135        // v7.12.6 — optional DECLARE prelude.
7136        let declarations = if matches!(
7137            self.peek(),
7138            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7139        ) {
7140            self.advance();
7141            self.parse_plpgsql_declare_block()?
7142        } else {
7143            Vec::new()
7144        };
7145        // BEGIN keyword (PL/pgSQL — distinct from the SQL
7146        // `BEGIN` transaction-start, but we can reuse the
7147        // reserved Token::Begin since the body is a separate
7148        // lex/parse context).
7149        if !matches!(self.peek(), Token::Begin) {
7150            return Err(self.err(alloc::format!(
7151                "expected BEGIN at start of plpgsql block, got {:?}",
7152                self.peek()
7153            )));
7154        }
7155        self.advance();
7156        let statements = self.parse_plpgsql_stmt_list_until_end()?;
7157        // v7.37.20 (20.10) — optional EXCEPTION clause between the
7158        // body's last statement and the trailing END. When present
7159        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7160        // arms terminated by END.
7161        let exception_handlers = if matches!(
7162            self.peek(),
7163            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7164        ) {
7165            self.advance();
7166            self.parse_plpgsql_exception_handlers()?
7167        } else {
7168            Vec::new()
7169        };
7170        Ok(PlPgSqlBlock {
7171            declarations,
7172            statements,
7173            exception_handlers,
7174        })
7175    }
7176
7177    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7178    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7179    fn parse_plpgsql_exception_handlers(
7180        &mut self,
7181    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7182        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7183        loop {
7184            // Stop at END — the block-level trailing END LOOP / END;
7185            // is handled by the caller.
7186            if matches!(
7187                self.peek(),
7188                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7189            ) {
7190                return Ok(out);
7191            }
7192            // WHEN <cond> [OR <cond>]* THEN <body>
7193            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7194            {
7195                return Err(self.err(alloc::format!(
7196                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
7197                    self.peek()
7198                )));
7199            }
7200            self.advance();
7201            let mut conditions: Vec<String> = Vec::new();
7202            conditions.push(self.expect_ident_like()?);
7203            while matches!(self.peek(), Token::Or) {
7204                self.advance();
7205                conditions.push(self.expect_ident_like()?);
7206            }
7207            let then_kw = self.expect_ident_like()?;
7208            if !then_kw.eq_ignore_ascii_case("then") {
7209                return Err(self.err(alloc::format!(
7210                    "expected THEN after WHEN condition list, got {then_kw:?}"
7211                )));
7212            }
7213            let body = self.parse_plpgsql_stmt_list_until_end()?;
7214            out.push(crate::ast::ExceptionHandler { conditions, body });
7215        }
7216    }
7217
7218    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7219    /// prelude. Caller has already consumed `DECLARE`. We stop
7220    /// reading entries when we hit `BEGIN`.
7221    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7222        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7223        loop {
7224            if matches!(self.peek(), Token::Begin) {
7225                return Ok(out);
7226            }
7227            let name = self.expect_ident_like()?;
7228            // v7.37.20 (20.7) — type inference: if the next token is
7229            // `:=` or `=` (no explicit type), infer from the default
7230            // expression. Otherwise the ident that follows is the
7231            // declared type.
7232            //
7233            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7234            // (PG-standard). SPG parse-accepts and treats identically
7235            // to inference — the eventual runtime value determines
7236            // the local's type, which is faithful to how SPG handles
7237            // untyped locals today (see 20.7). Full compile-time
7238            // catalog lookup queues with v7.40 PL/pgSQL epic.
7239            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7240                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7241                // downstream declaration walker to type the local by
7242                // the runtime type of the default expression.
7243                FunctionArgType::Raw("_infer_".into())
7244            } else {
7245                let ty_token = self.expect_ident_like()?;
7246                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7247                // consume optional `.<ident>` qualifier + `%<KW>`
7248                // suffix. Both qualifier and suffix map to _infer_.
7249                if matches!(self.peek(), Token::Dot) {
7250                    self.advance();
7251                    let _ = self.expect_ident_like()?;
7252                }
7253                if matches!(self.peek(), Token::Percent) {
7254                    self.advance();
7255                    // Consume the trailing TYPE / ROWTYPE ident.
7256                    let _ = self.expect_ident_like()?;
7257                    FunctionArgType::Raw("_infer_".into())
7258                } else {
7259                    match map_type_ident_to_column_type_name(&ty_token) {
7260                        Some(t) => FunctionArgType::Typed(t),
7261                        None => FunctionArgType::Raw(ty_token),
7262                    }
7263                }
7264            };
7265            let default = match self.peek() {
7266                Token::ColonEq => {
7267                    self.advance();
7268                    Some(self.parse_expr(0)?)
7269                }
7270                Token::Eq => {
7271                    // PL/pgSQL also accepts `=` for the
7272                    // DECLARE default (PG treats them the same
7273                    // in this position).
7274                    self.advance();
7275                    Some(self.parse_expr(0)?)
7276                }
7277                _ => None,
7278            };
7279            // Mandatory `;` between declarations.
7280            if !matches!(self.peek(), Token::Semicolon) {
7281                return Err(self.err(alloc::format!(
7282                    "expected ; after DECLARE entry for {name:?}, got {:?}",
7283                    self.peek()
7284                )));
7285            }
7286            self.advance();
7287            out.push(PlPgSqlDeclare { name, ty, default });
7288        }
7289    }
7290
7291    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7292    /// the terminating `END;` (or `END IF;` etc — handled by the
7293    /// per-construct sub-parsers). Used by both the outer block
7294    /// and the IF/ELSE branch bodies.
7295    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7296        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7297        loop {
7298            // Allow trailing semicolons + END.
7299            while matches!(self.peek(), Token::Semicolon) {
7300                self.advance();
7301            }
7302            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7303            if matches!(
7304                self.peek(),
7305                Token::Ident(s) | Token::QuotedIdent(s)
7306                    if s.eq_ignore_ascii_case("end")
7307                        || s.eq_ignore_ascii_case("else")
7308                        || s.eq_ignore_ascii_case("elsif")
7309                        || s.eq_ignore_ascii_case("elseif")
7310                        || s.eq_ignore_ascii_case("exception")
7311                        || s.eq_ignore_ascii_case("when")
7312            ) {
7313                return Ok(statements);
7314            }
7315            // Otherwise: one statement, then expect `;` or
7316            // a block-terminator keyword.
7317            let stmt = self.parse_plpgsql_stmt()?;
7318            statements.push(stmt);
7319            match self.peek() {
7320                Token::Semicolon => {
7321                    self.advance();
7322                }
7323                Token::Ident(s) | Token::QuotedIdent(s)
7324                    if s.eq_ignore_ascii_case("end")
7325                        || s.eq_ignore_ascii_case("else")
7326                        || s.eq_ignore_ascii_case("elsif")
7327                        || s.eq_ignore_ascii_case("elseif")
7328                        || s.eq_ignore_ascii_case("exception")
7329                        || s.eq_ignore_ascii_case("when") =>
7330                {
7331                    // Final statement of the block without `;`.
7332                }
7333                other => {
7334                    return Err(self.err(alloc::format!(
7335                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7336                    )));
7337                }
7338            }
7339        }
7340    }
7341
7342    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7343        // RETURN keyword?
7344        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7345        {
7346            self.advance();
7347            return self.parse_plpgsql_return();
7348        }
7349        // v7.12.6 — IF block.
7350        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7351        {
7352            self.advance();
7353            return self.parse_plpgsql_if();
7354        }
7355        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7356        // Detected by peeking that token pos+3 is Ident("execute").
7357        if matches!(self.peek(), Token::For)
7358            && matches!(
7359                self.tokens.get(self.pos + 1),
7360                Some(Token::Ident(_) | Token::QuotedIdent(_))
7361            )
7362            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7363            && matches!(
7364                self.tokens.get(self.pos + 3),
7365                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7366            )
7367        {
7368            self.advance(); // FOR
7369            let var = self.expect_ident_like()?;
7370            self.advance(); // IN
7371            self.advance(); // EXECUTE
7372            // Prescan for LOOP at paren depth 0 so parse_expr stops
7373            // before the LOOP keyword (same trick as the bare-SELECT
7374            // ForQuery arm).
7375            let mut depth: i32 = 0;
7376            let mut loop_pos: Option<usize> = None;
7377            let mut scan = self.pos;
7378            while scan < self.tokens.len() {
7379                match self.tokens.get(scan) {
7380                    Some(Token::LParen) => depth += 1,
7381                    Some(Token::RParen) => depth -= 1,
7382                    Some(Token::Ident(s) | Token::QuotedIdent(s))
7383                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7384                    {
7385                        loop_pos = Some(scan);
7386                        break;
7387                    }
7388                    _ => {}
7389                }
7390                scan += 1;
7391            }
7392            let loop_pos = loop_pos.ok_or_else(|| {
7393                self.err(alloc::format!(
7394                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7395                ))
7396            })?;
7397            let saved_loop = self.tokens[loop_pos].clone();
7398            self.tokens[loop_pos] = Token::Semicolon;
7399            let expr_result = self.parse_expr(0);
7400            self.tokens[loop_pos] = saved_loop;
7401            let sql_expr = expr_result?;
7402            let loop_kw = self.expect_ident_like()?;
7403            if !loop_kw.eq_ignore_ascii_case("loop") {
7404                return Err(self.err(alloc::format!(
7405                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7406                )));
7407            }
7408            let body = self.parse_plpgsql_stmt_list_until_end()?;
7409            let end_kw = self.expect_ident_like()?;
7410            if !end_kw.eq_ignore_ascii_case("end") {
7411                return Err(self.err(alloc::format!(
7412                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7413                )));
7414            }
7415            let loop_kw2 = self.expect_ident_like()?;
7416            if !loop_kw2.eq_ignore_ascii_case("loop") {
7417                return Err(self.err(alloc::format!(
7418                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7419                )));
7420            }
7421            return Ok(PlPgSqlStmt::ForExecute {
7422                var,
7423                sql_expr,
7424                body,
7425            });
7426        }
7427        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7428        //
7429        // Two syntactic forms:
7430        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7431        //   FOR var IN (SELECT ...) LOOP ...
7432        //
7433        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7434        // the trailing `LOOP` keyword as a table alias, we prescan
7435        // forward to find LOOP at paren depth 0, splice a fake
7436        // Semicolon at that position (so SELECT parses cleanly),
7437        // then re-splice LOOP back in.
7438        //
7439        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7440        // LOOP directly — no scan required.
7441        if matches!(self.peek(), Token::For)
7442            && matches!(
7443                self.tokens.get(self.pos + 1),
7444                Some(Token::Ident(_) | Token::QuotedIdent(_))
7445            )
7446            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7447            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7448                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7449        {
7450            self.advance(); // FOR
7451            let var = self.expect_ident_like()?;
7452            // IN
7453            self.advance();
7454            let query = if matches!(self.peek(), Token::LParen) {
7455                // Paren-wrapped SELECT.
7456                self.advance();
7457                let inner = self.parse_select_stmt()?;
7458                let Statement::Select(q) = inner else {
7459                    return Err(self.err(alloc::format!(
7460                        "expected SELECT inside (…), got {:?}",
7461                        self.peek()
7462                    )));
7463                };
7464                if !matches!(self.peek(), Token::RParen) {
7465                    return Err(self.err(alloc::format!(
7466                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7467                        self.peek()
7468                    )));
7469                }
7470                self.advance();
7471                q
7472            } else {
7473                // Bare SELECT: prescan to find the LOOP boundary.
7474                let mut depth: i32 = 0;
7475                let mut loop_pos: Option<usize> = None;
7476                let mut scan = self.pos;
7477                while scan < self.tokens.len() {
7478                    match self.tokens.get(scan) {
7479                        Some(Token::LParen) => depth += 1,
7480                        Some(Token::RParen) => depth -= 1,
7481                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7482                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7483                        {
7484                            loop_pos = Some(scan);
7485                            break;
7486                        }
7487                        _ => {}
7488                    }
7489                    scan += 1;
7490                }
7491                let loop_pos = loop_pos.ok_or_else(|| {
7492                    self.err(alloc::format!(
7493                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7494                    ))
7495                })?;
7496                // Swap the LOOP token with a synthetic Semicolon so
7497                // parse_select_stmt stops there, then restore afterward.
7498                let saved_loop = self.tokens[loop_pos].clone();
7499                self.tokens[loop_pos] = Token::Semicolon;
7500                let parse_result = self.parse_select_stmt();
7501                self.tokens[loop_pos] = saved_loop;
7502                let inner = parse_result?;
7503                let Statement::Select(q) = inner else {
7504                    return Err(self.err(alloc::format!(
7505                        "expected SELECT after FOR <var> IN, got {:?}",
7506                        self.peek()
7507                    )));
7508                };
7509                q
7510            };
7511            let loop_kw = self.expect_ident_like()?;
7512            if !loop_kw.eq_ignore_ascii_case("loop") {
7513                return Err(self.err(alloc::format!(
7514                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7515                )));
7516            }
7517            let body = self.parse_plpgsql_stmt_list_until_end()?;
7518            let end_kw = self.expect_ident_like()?;
7519            if !end_kw.eq_ignore_ascii_case("end") {
7520                return Err(self.err(alloc::format!(
7521                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7522                )));
7523            }
7524            let loop_kw2 = self.expect_ident_like()?;
7525            if !loop_kw2.eq_ignore_ascii_case("loop") {
7526                return Err(self.err(alloc::format!(
7527                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7528                )));
7529            }
7530            return Ok(PlPgSqlStmt::ForQuery {
7531                var,
7532                query: Box::new(query),
7533                body,
7534            });
7535        }
7536        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7537        // FOR is a reserved keyword token (Token::For).
7538        if matches!(self.peek(), Token::For)
7539            && matches!(
7540                self.tokens.get(self.pos + 1),
7541                Some(Token::Ident(_) | Token::QuotedIdent(_))
7542            )
7543            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7544        {
7545            self.advance(); // FOR
7546            let var = self.expect_ident_like()?;
7547            if !matches!(self.peek(), Token::In) {
7548                return Err(self.err(alloc::format!(
7549                    "expected IN after FOR <var>, got {:?}",
7550                    self.peek()
7551                )));
7552            }
7553            self.advance();
7554            let reverse = matches!(
7555                self.peek(),
7556                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7557            );
7558            if reverse {
7559                self.advance();
7560            }
7561            let start = self.parse_expr(0)?;
7562            if !matches!(self.peek(), Token::DotDot) {
7563                return Err(self.err(alloc::format!(
7564                    "expected '..' between FOR loop bounds, got {:?}",
7565                    self.peek()
7566                )));
7567            }
7568            self.advance();
7569            let end = self.parse_expr(0)?;
7570            let loop_kw = self.expect_ident_like()?;
7571            if !loop_kw.eq_ignore_ascii_case("loop") {
7572                return Err(self.err(alloc::format!(
7573                    "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7574                )));
7575            }
7576            let body = self.parse_plpgsql_stmt_list_until_end()?;
7577            let end_kw = self.expect_ident_like()?;
7578            if !end_kw.eq_ignore_ascii_case("end") {
7579                return Err(self.err(alloc::format!(
7580                    "expected END LOOP after FOR body, got {end_kw:?}"
7581                )));
7582            }
7583            let loop_kw2 = self.expect_ident_like()?;
7584            if !loop_kw2.eq_ignore_ascii_case("loop") {
7585                return Err(self.err(alloc::format!(
7586                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7587                )));
7588            }
7589            return Ok(PlPgSqlStmt::ForRange {
7590                var,
7591                start,
7592                end,
7593                reverse,
7594                body,
7595            });
7596        }
7597        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7598        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7599        {
7600            self.advance();
7601            let body = self.parse_plpgsql_stmt_list_until_end()?;
7602            let end_kw = self.expect_ident_like()?;
7603            if !end_kw.eq_ignore_ascii_case("end") {
7604                return Err(self.err(alloc::format!(
7605                    "expected END LOOP after LOOP body, got {end_kw:?}"
7606                )));
7607            }
7608            let loop_kw = self.expect_ident_like()?;
7609            if !loop_kw.eq_ignore_ascii_case("loop") {
7610                return Err(self.err(alloc::format!(
7611                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7612                )));
7613            }
7614            return Ok(PlPgSqlStmt::Loop { body });
7615        }
7616        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7617        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7618        {
7619            self.advance();
7620            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7621            {
7622                self.advance();
7623                Some(self.parse_expr(0)?)
7624            } else {
7625                None
7626            };
7627            return Ok(PlPgSqlStmt::Exit { when });
7628        }
7629        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7630        // already-parsed Statement or a runtime-computed SQL string.
7631        // The disambiguator vs the extended-query-protocol `EXECUTE
7632        // <stmt_name>` (which is a top-level Statement, not a
7633        // plpgsql line) is that inside a DO block / trigger body the
7634        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7635        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7636        {
7637            self.advance();
7638            let sql = self.parse_expr(0)?;
7639            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7640        }
7641        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7642        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7643        {
7644            self.advance();
7645            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7646            {
7647                self.advance();
7648                Some(self.parse_expr(0)?)
7649            } else {
7650                None
7651            };
7652            return Ok(PlPgSqlStmt::Continue { when });
7653        }
7654        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7655        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7656        {
7657            self.advance();
7658            let condition = self.parse_expr(0)?;
7659            let loop_kw = self.expect_ident_like()?;
7660            if !loop_kw.eq_ignore_ascii_case("loop") {
7661                return Err(self.err(alloc::format!(
7662                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7663                )));
7664            }
7665            let body = self.parse_plpgsql_stmt_list_until_end()?;
7666            // Expect END LOOP.
7667            let end_kw = self.expect_ident_like()?;
7668            if !end_kw.eq_ignore_ascii_case("end") {
7669                return Err(self.err(alloc::format!(
7670                    "expected END LOOP after WHILE body, got {end_kw:?}"
7671                )));
7672            }
7673            let loop_kw2 = self.expect_ident_like()?;
7674            if !loop_kw2.eq_ignore_ascii_case("loop") {
7675                return Err(self.err(alloc::format!(
7676                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7677                )));
7678            }
7679            return Ok(PlPgSqlStmt::While { condition, body });
7680        }
7681        // v7.12.6 — RAISE.
7682        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7683        {
7684            self.advance();
7685            return self.parse_plpgsql_raise();
7686        }
7687        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7688        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7689        {
7690            self.advance();
7691            let condition = self.parse_expr(0)?;
7692            let message = if matches!(self.peek(), Token::Comma) {
7693                self.advance();
7694                Some(self.parse_expr(0)?)
7695            } else {
7696                None
7697            };
7698            return Ok(PlPgSqlStmt::Assert { condition, message });
7699        }
7700        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7701        //   "PERFORM is equivalent to SELECT but discards the
7702        //    result." Side effects (function calls, RAISE inside
7703        //    SQL functions, etc.) still execute. We desugar to
7704        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7705        //    existing embedded-statement path handles execution +
7706        //    result-discard cleanly. The result is naturally
7707        //    discarded because EmbeddedSql doesn't propagate row
7708        //    sets back to the plpgsql interpreter.
7709        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7710        {
7711            self.advance();
7712            // Splice a synthetic Token::Select into the stream at
7713            // the current position so parse_select_stmt parses the
7714            // remainder as a normal SELECT body. Token-stream
7715            // surgery mirrors the try_parse_plpgsql_select_into
7716            // pattern used for SELECT … INTO desugaring.
7717            self.tokens.insert(self.pos, Token::Select);
7718            let select = self.parse_select_stmt()?;
7719            let Statement::Select(s) = select else {
7720                return Err(self.err(alloc::format!(
7721                    "expected SELECT body after PERFORM, got {:?}",
7722                    self.peek()
7723                )));
7724            };
7725            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7726        }
7727        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7728        // plpgsql-specific shape (mailrs round-10 migrate-042).
7729        // PG's SELECT INTO at top-level SQL would CREATE a new
7730        // table; inside plpgsql it ASSIGNS the query result to
7731        // a local variable. We detect the INTO at paren-depth
7732        // 0 between SELECT and the statement boundary; if
7733        // found, split the token stream into "pre-INTO
7734        // projection" + "var" + "post-INTO FROM/WHERE…" and
7735        // rebuild as a SelectInto with a regular SELECT body
7736        // (no INTO clause).
7737        if matches!(self.peek(), Token::Select)
7738            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7739        {
7740            return Ok(PlPgSqlStmt::SelectInto {
7741                var: var_name,
7742                body: Box::new(select_body),
7743            });
7744        }
7745        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7746        // SELECT can appear directly inside a trigger body; we
7747        // recurse into the regular Statement parser, which will
7748        // stop at the trailing `;` (which our caller then
7749        // consumes).
7750        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7751        // also embed ALTER / CREATE / DROP statements; route
7752        // those through the same parser so the DO body parses
7753        // cleanly.
7754        if matches!(self.peek(), Token::Insert)
7755            || matches!(self.peek(), Token::Select)
7756            || matches!(self.peek(), Token::Create)
7757            || matches!(self.peek(), Token::Drop)
7758            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7759                if s.eq_ignore_ascii_case("update")
7760                    || s.eq_ignore_ascii_case("delete")
7761                    || s.eq_ignore_ascii_case("alter"))
7762        {
7763            let stmt = self.parse_one_statement()?;
7764            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7765        }
7766        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7767        // followed by `:=` and an expression.
7768        let target = self.parse_plpgsql_assign_target()?;
7769        // PL/pgSQL assignment uses `:=`. The lexer represents
7770        // this as a colon followed by `=`; check both shapes.
7771        match self.peek() {
7772            Token::ColonEq => {
7773                self.advance();
7774            }
7775            Token::Colon => {
7776                self.advance();
7777                if !matches!(self.peek(), Token::Eq) {
7778                    return Err(self.err(alloc::format!(
7779                        "expected := after plpgsql assign target, got `:` then {:?}",
7780                        self.peek()
7781                    )));
7782                }
7783                self.advance();
7784            }
7785            other => {
7786                return Err(self.err(alloc::format!(
7787                    "expected := after plpgsql assign target, got {other:?}"
7788                )));
7789            }
7790        }
7791        let value = self.parse_expr(0)?;
7792        Ok(PlPgSqlStmt::Assign { target, value })
7793    }
7794
7795    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7796    /// [ELSE body] END IF`. `IF` keyword already consumed.
7797    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7798        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7799        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7800        loop {
7801            // <expr> THEN
7802            let cond = self.parse_expr(0)?;
7803            let then_kw = self.expect_ident_like()?;
7804            if !then_kw.eq_ignore_ascii_case("then") {
7805                return Err(self.err(alloc::format!(
7806                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7807                )));
7808            }
7809            let body = self.parse_plpgsql_stmt_list_until_end()?;
7810            branches.push((cond, body));
7811            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7812            match self.peek() {
7813                Token::Ident(s) | Token::QuotedIdent(s)
7814                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7815                {
7816                    self.advance();
7817                    continue;
7818                }
7819                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7820                    self.advance();
7821                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7822                    break;
7823                }
7824                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7825                    break;
7826                }
7827                other => {
7828                    return Err(self.err(alloc::format!(
7829                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7830                    )));
7831                }
7832            }
7833        }
7834        // Expect `END IF` (the END keyword is the one we're
7835        // looking at right now).
7836        let end_kw = self.expect_ident_like()?;
7837        if !end_kw.eq_ignore_ascii_case("end") {
7838            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7839        }
7840        let if_kw = self.expect_ident_like()?;
7841        if !if_kw.eq_ignore_ascii_case("if") {
7842            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7843        }
7844        Ok(PlPgSqlStmt::If {
7845            branches,
7846            else_branch,
7847        })
7848    }
7849
7850    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7851    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7852    /// is already consumed.
7853    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7854        let lvl_ident = self.expect_ident_like()?;
7855        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7856            "notice" => RaiseLevel::Notice,
7857            "warning" => RaiseLevel::Warning,
7858            "info" => RaiseLevel::Info,
7859            "log" => RaiseLevel::Log,
7860            "debug" => RaiseLevel::Debug,
7861            "exception" => RaiseLevel::Exception,
7862            other => {
7863                return Err(self.err(alloc::format!(
7864                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7865                )));
7866            }
7867        };
7868        // Message: required for v7.12.6. PG accepts a bare
7869        // RAISE-rethrow form (no message), reserved for future
7870        // RAISE-no-args support.
7871        let Token::String(msg) = self.peek() else {
7872            return Err(self.err(alloc::format!(
7873                "expected RAISE message string, got {:?}",
7874                self.peek()
7875            )));
7876        };
7877        let message = msg.clone();
7878        self.advance();
7879        // Optional comma-separated args (PG `%` format substitution).
7880        let mut args: Vec<Expr> = Vec::new();
7881        while matches!(self.peek(), Token::Comma) {
7882            self.advance();
7883            args.push(self.parse_expr(0)?);
7884        }
7885        Ok(PlPgSqlStmt::Raise {
7886            level,
7887            message,
7888            args,
7889        })
7890    }
7891
7892    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7893    /// <projection> INTO <var> [FROM …]` (mailrs round-10
7894    /// migrate-042). Returns `(rebuilt_select_without_into,
7895    /// var_name)` when the pattern matches; `None` for
7896    /// regular SELECTs (those go through the embedded-SQL
7897    /// path). Token-stream surgery so the rebuilt SELECT
7898    /// parses through the regular `parse_select_stmt`.
7899    #[allow(clippy::too_many_lines)]
7900    fn try_parse_plpgsql_select_into(
7901        &mut self,
7902    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7903        // Scan forward from `self.pos + 1` (past Token::Select)
7904        // for Token::Into at paren-depth 0, stopping at the
7905        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7906        // end the plpgsql statement.
7907        let start = self.pos;
7908        let mut into_pos: Option<usize> = None;
7909        let mut depth: i32 = 0;
7910        let mut i = start + 1;
7911        while i < self.tokens.len() {
7912            match &self.tokens[i] {
7913                Token::LParen => depth += 1,
7914                Token::RParen => depth -= 1,
7915                Token::Semicolon if depth == 0 => break,
7916                Token::Ident(s)
7917                    if depth == 0
7918                        && (s.eq_ignore_ascii_case("end")
7919                            || s.eq_ignore_ascii_case("else")
7920                            || s.eq_ignore_ascii_case("elsif")) =>
7921                {
7922                    break;
7923                }
7924                Token::Into if depth == 0 => {
7925                    into_pos = Some(i);
7926                    break;
7927                }
7928                _ => {}
7929            }
7930            i += 1;
7931        }
7932        let Some(into_at) = into_pos else {
7933            return Ok(None);
7934        };
7935        // The token immediately after INTO must be the target
7936        // var ident; anything else (e.g. INSERT INTO table)
7937        // ruled out by the depth-0 check above. Capture it.
7938        let var = match self.tokens.get(into_at + 1) {
7939            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7940            other => {
7941                return Err(self.err(alloc::format!(
7942                    "expected variable name after SELECT … INTO, got {other:?}"
7943                )));
7944            }
7945        };
7946        // Find the end of the plpgsql SELECT INTO statement —
7947        // same boundary rules as the depth-0 scan above.
7948        let mut end = into_at + 2;
7949        let mut depth2: i32 = 0;
7950        while end < self.tokens.len() {
7951            match &self.tokens[end] {
7952                Token::LParen => depth2 += 1,
7953                Token::RParen => depth2 -= 1,
7954                Token::Semicolon if depth2 == 0 => break,
7955                Token::Ident(s)
7956                    if depth2 == 0
7957                        && (s.eq_ignore_ascii_case("end")
7958                            || s.eq_ignore_ascii_case("else")
7959                            || s.eq_ignore_ascii_case("elsif")) =>
7960                {
7961                    break;
7962                }
7963                _ => {}
7964            }
7965            end += 1;
7966        }
7967        // Rebuild a token stream that represents the SELECT
7968        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7969        // post-var tokens up to statement end]. Run the
7970        // regular `parse_select_stmt` against it.
7971        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7972        for j in start..into_at {
7973            rebuilt.push(self.tokens[j].clone());
7974        }
7975        for j in (into_at + 2)..end {
7976            rebuilt.push(self.tokens[j].clone());
7977        }
7978        rebuilt.push(Token::Eof);
7979        let saved_pos = self.pos;
7980        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7981        self.pos = 0;
7982        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7983        if !matches!(self.peek(), Token::Select) {
7984            self.tokens = saved_tokens;
7985            self.pos = saved_pos;
7986            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7987        }
7988        let sel = self.parse_select_stmt();
7989        self.tokens = saved_tokens;
7990        self.pos = end;
7991        let sel = sel?;
7992        let Statement::Select(body) = sel else {
7993            return Err(self.err(alloc::format!(
7994                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7995            )));
7996        };
7997        Ok(Some((body, var)))
7998    }
7999
8000    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
8001        // v7.16.1 — read the head token DIRECTLY rather than
8002        // via `expect_ident_like`. The v7.14.0 schema-qualifier
8003        // strip (`public.t` → `t`) inside `expect_ident_like`
8004        // greedily consumes any `ident . ident` pair, which
8005        // silently turned every `NEW.col := …` /
8006        // `OLD.col := …` plpgsql assignment into a Local("col")
8007        // assignment — the head "new"/"old" was eaten as if it
8008        // were a schema name and the Dot was consumed too, so
8009        // this function's own `peek() == Token::Dot` check
8010        // below never fired. Every BEFORE trigger that rewrote
8011        // a NEW cell was a silent no-op for two major releases
8012        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
8013        // gate failures were investigated as v7.16.1 backlog.
8014        let head = match self.advance() {
8015            Token::Ident(s) | Token::QuotedIdent(s) => s,
8016            other => {
8017                return Err(self.err(alloc::format!(
8018                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
8019                )));
8020            }
8021        };
8022        if matches!(self.peek(), Token::Dot) {
8023            self.advance();
8024            let col = self.expect_ident_like()?;
8025            if head.eq_ignore_ascii_case("new") {
8026                return Ok(AssignTarget::NewColumn(col));
8027            }
8028            if head.eq_ignore_ascii_case("old") {
8029                return Ok(AssignTarget::OldColumn(col));
8030            }
8031            return Err(self.err(alloc::format!(
8032                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
8033                 got {head:?}.<col>"
8034            )));
8035        }
8036        Ok(AssignTarget::Local(head))
8037    }
8038
8039    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
8040        // RETURN NEW / OLD / NULL — bare-ident forms.
8041        match self.peek() {
8042            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
8043                self.advance();
8044                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
8045            }
8046            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
8047                self.advance();
8048                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
8049            }
8050            Token::Null => {
8051                self.advance();
8052                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8053            }
8054            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
8055            // per PL/pgSQL convention.
8056            Token::Semicolon => {
8057                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8058            }
8059            _ => {}
8060        }
8061        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
8062        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
8063        // caller-visible effect (blocks don't return sets), so we
8064        // desugar it identically to PERFORM: parse the SELECT (or
8065        // EXECUTE dynamic) as embedded SQL that runs for side
8066        // effects and discards the result. RETURN NEXT <expr>
8067        // (single-row accumulator) queues with v7.40 SETOF function
8068        // infrastructure.
8069        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
8070        // and keep going.
8071        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
8072        {
8073            self.advance();
8074            let e = self.parse_expr(0)?;
8075            return Ok(PlPgSqlStmt::ReturnNext(e));
8076        }
8077        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
8078        {
8079            self.advance();
8080            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
8081            // rows go to the set, like the static form. It used to desugar to a
8082            // bare ExecuteDynamic, whose result was DISCARDED.
8083            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
8084            {
8085                self.advance();
8086                let sql = self.parse_expr(0)?;
8087                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
8088            }
8089            // Bare RETURN QUERY <select>. If the current token is
8090            // not already SELECT (e.g., the user wrote `RETURN QUERY
8091            // <projection> FROM ...` in a shorthand — rare but PG
8092            // accepts a bare projection here), splice one in. Same
8093            // trick as PERFORM.
8094            if !matches!(self.peek(), Token::Select) {
8095                self.tokens.insert(self.pos, Token::Select);
8096            }
8097            let select = self.parse_select_stmt()?;
8098            let Statement::Select(s) = select else {
8099                return Err(self.err(alloc::format!(
8100                    "expected SELECT body after RETURN QUERY, got {:?}",
8101                    self.peek()
8102                )));
8103            };
8104            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
8105            // to an embedded side-effect SELECT whose rows were DISCARDED, which
8106            // in a SETOF function is the entire answer thrown away.
8107            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
8108        }
8109        // Fall through: parse a full expression.
8110        let e = self.parse_expr(0)?;
8111        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
8112    }
8113
8114    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
8115        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
8116        // are ident-shaped (the parser keys off case-insensitive
8117        // match — same shape used by the top-level Update / Delete
8118        // dispatchers at parse_one_statement).
8119        if matches!(self.peek(), Token::Insert) {
8120            self.advance();
8121            return Ok(TriggerEvent::Insert);
8122        }
8123        match self.peek() {
8124            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
8125                self.advance();
8126                Ok(TriggerEvent::Update)
8127            }
8128            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
8129                self.advance();
8130                Ok(TriggerEvent::Delete)
8131            }
8132            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
8133                self.advance();
8134                Ok(TriggerEvent::Truncate)
8135            }
8136            other => Err(self.err(alloc::format!(
8137                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8138            ))),
8139        }
8140    }
8141
8142    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8143    ///   - (no clause) → implicit `FOR ALL TABLES`
8144    ///   - `FOR ALL TABLES`
8145    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8146    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8147    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
8148    ///     REJECTS the bare plural (`invalid publication object list`,
8149    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
8150    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8151    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8152        let name = self.expect_ident_or_string()?;
8153        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8154        // shape so existing publications keep parsing identically.
8155        let scope = if matches!(self.peek(), Token::For) {
8156            self.advance();
8157            if matches!(self.peek(), Token::All) {
8158                self.advance();
8159                if !matches!(self.peek(), Token::Tables) {
8160                    return Err(self.err(format!(
8161                        "expected TABLES after FOR ALL, got {:?}",
8162                        self.peek()
8163                    )));
8164                }
8165                self.advance();
8166                if matches!(self.peek(), Token::Except) {
8167                    self.advance();
8168                    let tables = self.parse_publication_table_list()?;
8169                    PublicationScope::AllTablesExcept(tables)
8170                } else {
8171                    PublicationScope::AllTables
8172                }
8173            } else if matches!(self.peek(), Token::Table) {
8174                self.advance();
8175                let tables = self.parse_publication_table_list()?;
8176                PublicationScope::ForTables(tables)
8177            } else if matches!(self.peek(), Token::Tables) {
8178                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8179                // plural (`FOR TABLES t`) is REJECTED (`invalid
8180                // publication object list`); TABLES only pairs with
8181                // `IN SCHEMA`. The old arm accepted it on an
8182                // unverifiable "PG 19 accepts both" claim.
8183                self.advance();
8184                if !matches!(self.peek(), Token::In) {
8185                    return Err(self.err(alloc::string::String::from(
8186                        "invalid publication object list",
8187                    )));
8188                }
8189                self.advance();
8190                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8191                    return Err(self.err(format!(
8192                        "expected SCHEMA after FOR TABLES IN, got {:?}",
8193                        self.peek()
8194                    )));
8195                }
8196                self.advance();
8197                let schema = self.expect_ident_or_string()?;
8198                PublicationScope::TablesInSchema(schema)
8199            } else {
8200                return Err(self.err(format!(
8201                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8202                    self.peek()
8203                )));
8204            }
8205        } else {
8206            PublicationScope::AllTables
8207        };
8208        Ok(Statement::CreatePublication(CreatePublicationStatement {
8209            name,
8210            scope,
8211        }))
8212    }
8213
8214    /// v6.1.3 — Comma-separated identifier list for the publication
8215    /// FOR-clause. Requires at least one entry; empty list is a
8216    /// parse error (PG behaviour). Quoted idents are accepted; the
8217    /// names round-trip through `Display` as `quote_ident(name)`.
8218    ///
8219    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8220    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8221    /// pg_dump output. SPG's publication state today is per-table
8222    /// only (matching the pre-PG-15 surface); the col list + WHERE
8223    /// are parsed so dumps load through and the table name reaches
8224    /// `PublicationScope::ForTables`, but the filter is not enforced
8225    /// at publish time. Re-open when a customer dogfood gate
8226    /// requires per-row-filter or column-subset publish semantics
8227    /// (which gates on persistent slot state landing first, 21.12).
8228    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8229        let first = self.parse_publication_table_entry()?;
8230        let mut out = alloc::vec![first];
8231        while matches!(self.peek(), Token::Comma) {
8232            self.advance();
8233            out.push(self.parse_publication_table_entry()?);
8234        }
8235        Ok(out)
8236    }
8237
8238    /// One table entry inside a FOR TABLE clause:
8239    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8240    /// Returns just the table name; the column list + WHERE predicate
8241    /// are consumed and discarded per the parse-accept-discard
8242    /// commitment above.
8243    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8244        let name = self.expect_ident_like()?;
8245        // Optional column list — `(col, col, …)`.
8246        if matches!(self.peek(), Token::LParen) {
8247            self.advance();
8248            // Empty parens are a PG error too; require ≥ 1 column.
8249            let _ = self.expect_ident_like()?;
8250            while matches!(self.peek(), Token::Comma) {
8251                self.advance();
8252                let _ = self.expect_ident_like()?;
8253            }
8254            if !matches!(self.peek(), Token::RParen) {
8255                return Err(self.err(alloc::format!(
8256                    "expected ')' to close publication column list, got {:?}",
8257                    self.peek()
8258                )));
8259            }
8260            self.advance();
8261        }
8262        // Optional row filter — `WHERE (predicate)`.
8263        if matches!(self.peek(), Token::Where) {
8264            self.advance();
8265            if !matches!(self.peek(), Token::LParen) {
8266                return Err(self.err(alloc::format!(
8267                    "expected '(' after WHERE in publication row filter, got {:?}",
8268                    self.peek()
8269                )));
8270            }
8271            self.advance();
8272            let _ = self.parse_expr(0)?;
8273            if !matches!(self.peek(), Token::RParen) {
8274                return Err(self.err(alloc::format!(
8275                    "expected ')' to close publication WHERE filter, got {:?}",
8276                    self.peek()
8277                )));
8278            }
8279            self.advance();
8280        }
8281        Ok(name)
8282    }
8283
8284    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8285    ///                 CONNECTION '<conn>'
8286    ///                 PUBLICATION <pub> [, <pub> ...]`.
8287    ///
8288    /// The clause order is fixed (CONNECTION first, then
8289    /// PUBLICATION) to match PG. No WITH-options accepted in
8290    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8291    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8292        let name = self.expect_ident_or_string()?;
8293        if !matches!(self.peek(), Token::Connection) {
8294            return Err(self.err(format!(
8295                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8296                self.peek()
8297            )));
8298        }
8299        self.advance();
8300        let conn_str = self.expect_string_literal()?;
8301        if !matches!(self.peek(), Token::Publication) {
8302            return Err(self.err(format!(
8303                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8304                self.peek()
8305            )));
8306        }
8307        self.advance();
8308        // Reuse the publication FOR-list parser shape: at least one
8309        // identifier, comma-separated.
8310        let first = self.expect_ident_like()?;
8311        let mut publications = alloc::vec![first];
8312        while matches!(self.peek(), Token::Comma) {
8313            self.advance();
8314            publications.push(self.expect_ident_like()?);
8315        }
8316        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8317            name,
8318            conn_str,
8319            publications,
8320        }))
8321    }
8322
8323    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8324    /// All keywords after `WAIT` are bare idents in v6.1.x; no
8325    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8326    /// that fit `u64`.
8327    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8328    /// qualifier is a *namespace* the app owns (`app.user_id`,
8329    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8330    /// to discard. So parse the raw segments here instead of
8331    /// `expect_ident_like`, which strips a leading `schema.` qualifier
8332    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8333    /// a single segment and round-trip unchanged.
8334    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8335        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8336        loop {
8337            let seg = match self.advance() {
8338                Token::Ident(s) | Token::QuotedIdent(s) => s,
8339                other if unreserved_keyword_text(&other).is_some() => {
8340                    unreserved_keyword_text(&other).unwrap()
8341                }
8342                other => {
8343                    return Err(ParseError {
8344                        message: format!("expected parameter name, got {other:?}"),
8345                        token_pos: self.consumed_pos(),
8346                    });
8347                }
8348            };
8349            parts.push(seg);
8350            if matches!(self.peek(), Token::Dot) {
8351                self.advance();
8352                continue;
8353            }
8354            break;
8355        }
8356        Ok(parts.join(".").to_ascii_lowercase())
8357    }
8358
8359    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8360        Self::parse_set_value_inner(self)
8361    }
8362
8363    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8364        match self.advance() {
8365            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8366            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8367                Ok(crate::ast::SetValue::Default)
8368            }
8369            Token::Ident(s) | Token::QuotedIdent(s) => {
8370                let mut accum = s;
8371                while matches!(self.peek(), Token::Dot) {
8372                    self.advance();
8373                    let next = self.expect_ident_like()?;
8374                    accum.push('.');
8375                    accum.push_str(&next);
8376                }
8377                Ok(crate::ast::SetValue::Ident(accum))
8378            }
8379            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8380            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8381            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8382            // spellings that lex as keyword tokens, not idents:
8383            // `SET standard_conforming_strings = on` is in every
8384            // pg_dump preamble (`off` already lexes as an ident).
8385            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8386            // DEFAULT lexes as its keyword token, so the ident arm above
8387            // never saw it and the everyday reset form was a syntax error.
8388            Token::Default => Ok(crate::ast::SetValue::Default),
8389            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8390            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8391            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8392            // v7.14.0 — MySQL session/user variable RHS
8393            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8394            // Wrap as Ident so the SET handler can record it; the
8395            // engine treats `@VAR` / `@@VAR` values as opaque
8396            // strings.
8397            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8398            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8399            // is the common MySQL preamble shape. Allow a `+` or
8400            // `-` prefix on negative numerics for parity with PG
8401            // (some param defaults are negative).
8402            Token::Minus => match self.advance() {
8403                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8404                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8405                other => Err(self.err(format!(
8406                    "expected numeric after `-` in SET value, got {other:?}"
8407                ))),
8408            },
8409            other => Err(self.err(format!(
8410                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8411            ))),
8412        }
8413    }
8414
8415    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8416    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8417    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8418    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8419    /// present). Modes are comma-separated per PG; SPG also
8420    /// accepts space-separated for tolerance. READ ONLY / WRITE
8421    /// / DEFERRABLE are parsed-and-ignored (recorded for future
8422    /// surface but not behaviorally honoured today).
8423    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8424    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8425    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8426    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8427    /// session default rather than forcing READ COMMITTED.
8428    fn parse_isolation_level_clauses(
8429        &mut self,
8430    ) -> Result<crate::ast::TransactionModes, ParseError> {
8431        let mut level = IsolationLevel::default();
8432        let mut have_level = false;
8433        // v7.39 — READ ONLY / READ WRITE used to be consumed and dropped,
8434        // so `BEGIN READ ONLY` opened an ordinary read-write transaction.
8435        let mut read_only: Option<bool> = None;
8436        loop {
8437            // ISOLATION LEVEL …
8438            let saw_isolation =
8439                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8440            if saw_isolation {
8441                self.advance(); // ISOLATION
8442                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8443                    return Err(self.err(alloc::format!(
8444                        "expected LEVEL after ISOLATION, got {:?}",
8445                        self.peek()
8446                    )));
8447                }
8448                self.advance(); // LEVEL
8449                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8450                let w1 = self
8451                    .expect_ident_like()
8452                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8453                let lc = w1.to_ascii_lowercase();
8454                level = match lc.as_str() {
8455                    "serializable" => IsolationLevel::Serializable,
8456                    "repeatable" => {
8457                        // Expect READ
8458                        let w2 = self
8459                            .expect_ident_like()
8460                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8461                        if !w2.eq_ignore_ascii_case("read") {
8462                            return Err(self.err(alloc::format!(
8463                                "expected READ after REPEATABLE, got {w2:?}"
8464                            )));
8465                        }
8466                        IsolationLevel::RepeatableRead
8467                    }
8468                    "read" => {
8469                        let w2 = self
8470                            .expect_ident_like()
8471                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8472                        match w2.to_ascii_lowercase().as_str() {
8473                            "committed" => IsolationLevel::ReadCommitted,
8474                            "uncommitted" => IsolationLevel::ReadUncommitted,
8475                            other => {
8476                                return Err(self.err(alloc::format!(
8477                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8478                                )));
8479                            }
8480                        }
8481                    }
8482                    other => {
8483                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8484                    }
8485                };
8486                have_level = true;
8487            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8488                // v7.39 — READ ONLY | READ WRITE. The comment here used to
8489                // read "parsed, not behaviorally honoured", and it was
8490                // accurate: the clause was thrown away, so `BEGIN READ ONLY`
8491                // opened an ordinary read-write transaction and accepted
8492                // every write in it.
8493                self.advance();
8494                match self.peek().clone() {
8495                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8496                        self.advance();
8497                        read_only = Some(true);
8498                    }
8499                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8500                        self.advance();
8501                        read_only = Some(false);
8502                    }
8503                    other => {
8504                        return Err(self.err(alloc::format!(
8505                            "expected ONLY or WRITE after READ, got {other:?}"
8506                        )));
8507                    }
8508                }
8509            } else if matches!(self.peek(), Token::Not) {
8510                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8511                self.advance();
8512                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8513                    return Err(self.err(alloc::format!(
8514                        "expected DEFERRABLE after NOT, got {:?}",
8515                        self.peek()
8516                    )));
8517                }
8518                self.advance();
8519            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8520            {
8521                self.advance();
8522            } else {
8523                break;
8524            }
8525            // Optional comma between modes.
8526            if matches!(self.peek(), Token::Comma) {
8527                self.advance();
8528            }
8529        }
8530        Ok(crate::ast::TransactionModes {
8531            isolation: have_level.then_some(level),
8532            read_only,
8533        })
8534    }
8535
8536    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8537        // FOR is a v6.1.2-reserved keyword (Token::For). The
8538        // other two are bare idents — they've never needed lexer
8539        // support and we keep it that way.
8540        if !matches!(self.peek(), Token::For) {
8541            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8542        }
8543        self.advance();
8544        self.expect_keyword_ident("wal")?;
8545        self.expect_keyword_ident("position")?;
8546        let pos = self.expect_u64_literal()?;
8547        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8548        {
8549            self.advance();
8550            self.expect_keyword_ident("timeout")?;
8551            Some(self.expect_u64_literal()?)
8552        } else {
8553            None
8554        };
8555        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8556    }
8557
8558    /// v6.1.7 helper — consume a `Token::Integer` and check it
8559    /// fits `u64`. WAL positions and millisecond timeouts are
8560    /// non-negative.
8561    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8562        match self.advance() {
8563            Token::Integer(n) if n >= 0 => Ok(n as u64),
8564            Token::Integer(n) => Err(ParseError {
8565                message: format!("expected non-negative integer, got {n}"),
8566                token_pos: self.consumed_pos(),
8567            }),
8568            other => Err(ParseError {
8569                message: format!("expected integer literal, got {other:?}"),
8570                token_pos: self.consumed_pos(),
8571            }),
8572        }
8573    }
8574
8575    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8576    /// ROLE '<role>' (defaults to readonly). All string slots accept
8577    /// either a quoted ident or a quoted string literal.
8578    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8579    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8580    ///
8581    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8582    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8583    /// wire role) still parses — it is a different axis from the PG attributes.
8584    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8585    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8586    /// or RESET, so the plain attribute forms keep their old path.
8587    fn peeks_db_role_setting(&self) -> bool {
8588        let mut i = self.pos + 1; // past the object's name
8589        let word = |p: usize| -> Option<String> {
8590            match self.tokens.get(p) {
8591                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8592                Some(Token::In) => Some(String::from("in")),
8593                _ => None,
8594            }
8595        };
8596        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8597            i += 3; // IN DATABASE <name>
8598        }
8599        matches!(word(i).as_deref(), Some("set" | "reset"))
8600    }
8601
8602    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8603        use crate::ast::SetDbRoleSettingStatement;
8604        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8605        // identifier, so the ordinary name reader refuses it. Same trap
8606        // as TABLE / INDEX / FULL / DEFAULT before it.
8607        let name = if matches!(self.peek(), Token::All) {
8608            self.advance();
8609            String::from("all")
8610        } else {
8611            self.expect_ident_or_string()?
8612        };
8613        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8614        let all = name.eq_ignore_ascii_case("all");
8615        let (mut database, mut role) = if is_database {
8616            (Some(name), None)
8617        } else if all {
8618            (None, None)
8619        } else {
8620            (None, Some(name))
8621        };
8622        if matches!(self.peek(), Token::In) {
8623            self.advance();
8624            self.advance(); // DATABASE
8625            database = Some(self.expect_ident_or_string()?);
8626        }
8627        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8628        self.advance(); // SET | RESET
8629        if resetting && matches!(self.peek(), Token::All) {
8630            self.advance();
8631            self.consume_until_statement_boundary();
8632            return Ok(Statement::SetDbRoleSetting(Box::new(
8633                SetDbRoleSettingStatement {
8634                    database,
8635                    role,
8636                    param: None,
8637                    value: None,
8638                },
8639            )));
8640        }
8641        let param = self.expect_ident_like()?;
8642        let value = if resetting {
8643            None
8644        } else {
8645            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8646            // KEYWORD, so the ident-only check missed it and consumed
8647            // the word itself as the value — the same trap as ALL, one
8648            // clause over.
8649            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8650                self.advance();
8651            }
8652            Some(self.take_guc_value())
8653        };
8654        self.consume_until_statement_boundary();
8655        Ok(Statement::SetDbRoleSetting(Box::new(
8656            SetDbRoleSettingStatement {
8657                database,
8658                role,
8659                param: Some(param),
8660                value,
8661            },
8662        )))
8663    }
8664
8665    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8666    /// a quoted literal loses its quotes, a bare word or number does not.
8667    fn take_guc_value(&mut self) -> String {
8668        match self.advance() {
8669            Token::String(s) => s,
8670            Token::Integer(n) => format!("{n}"),
8671            Token::Float(f) => format!("{f}"),
8672            Token::Ident(s) | Token::QuotedIdent(s) => s,
8673            other => format!("{other:?}"),
8674        }
8675    }
8676
8677    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8678        let name = self.expect_ident_or_string()?;
8679        if self.peek_keyword_ident("with") {
8680            self.advance();
8681        }
8682        let mut password = String::new();
8683        let mut role = String::new();
8684        let mut login: Option<bool> = None;
8685        let mut inherit: Option<bool> = None;
8686        let mut superuser: Option<bool> = None;
8687        // Not a `while let`: the pattern would borrow `self` across the
8688        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8689        #[allow(clippy::while_let_loop)]
8690        loop {
8691            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8692                break;
8693            };
8694            match w.to_ascii_lowercase().as_str() {
8695                "password" => {
8696                    self.advance();
8697                    password = self.expect_string_literal()?;
8698                }
8699                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8700                // is the same slot.
8701                "encrypted" => {
8702                    self.advance();
8703                    self.expect_keyword_ident("password")?;
8704                    password = self.expect_string_literal()?;
8705                }
8706                "login" => {
8707                    self.advance();
8708                    login = Some(true);
8709                }
8710                "nologin" => {
8711                    self.advance();
8712                    login = Some(false);
8713                }
8714                "inherit" => {
8715                    self.advance();
8716                    inherit = Some(true);
8717                }
8718                "noinherit" => {
8719                    self.advance();
8720                    inherit = Some(false);
8721                }
8722                "superuser" => {
8723                    self.advance();
8724                    superuser = Some(true);
8725                }
8726                "nosuperuser" => {
8727                    self.advance();
8728                    superuser = Some(false);
8729                }
8730                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8731                "role" => {
8732                    self.advance();
8733                    role = self.expect_string_literal()?;
8734                }
8735                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8736                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8737                // accepted and ignored so a pg_dump role block restores. They
8738                // gate capabilities SPG does not have.
8739                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8740                | "noreplication" | "bypassrls" | "nobypassrls" => {
8741                    self.advance();
8742                }
8743                "connection" => {
8744                    self.advance();
8745                    self.expect_keyword_ident("limit")?;
8746                    self.advance(); // the number
8747                }
8748                "valid" => {
8749                    self.advance();
8750                    self.expect_keyword_ident("until")?;
8751                    self.expect_string_literal()?;
8752                }
8753                _ => break,
8754            }
8755        }
8756        if role.is_empty() {
8757            role = "readonly".to_string();
8758        }
8759        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8760            name,
8761            password,
8762            role,
8763            login,
8764            inherit,
8765            superuser,
8766            is_user,
8767        }))
8768    }
8769
8770    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8771    /// consumed the USING / WITH CHECK keyword.
8772    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8773        if !matches!(self.peek(), Token::LParen) {
8774            return Err(self.err(alloc::format!(
8775                "expected '(' after {clause}, got {:?}",
8776                self.peek()
8777            )));
8778        }
8779        self.advance();
8780        let e = self.parse_expr(0)?;
8781        if !matches!(self.peek(), Token::RParen) {
8782            return Err(self.err(alloc::format!(
8783                "expected ')' to close {clause}, got {:?}",
8784                self.peek()
8785            )));
8786        }
8787        self.advance();
8788        Ok(e)
8789    }
8790
8791    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8792    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8793        let mut roles = Vec::new();
8794        loop {
8795            roles.push(self.expect_ident_like()?);
8796            if matches!(self.peek(), Token::Comma) {
8797                self.advance();
8798            } else {
8799                break;
8800            }
8801        }
8802        Ok(roles)
8803    }
8804
8805    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8806    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8807    /// `CREATE POLICY`.
8808    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8809        use crate::ast::PolicyCmd;
8810        let name = self.expect_ident_like()?;
8811        if !matches!(self.peek(), Token::On) {
8812            return Err(self.err(alloc::format!(
8813                "expected ON after CREATE POLICY name, got {:?}",
8814                self.peek()
8815            )));
8816        }
8817        self.advance();
8818        let table = self.expect_ident_like()?;
8819
8820        let mut permissive = true;
8821        if matches!(self.peek(), Token::As) {
8822            self.advance();
8823            let w = self.expect_ident_like()?;
8824            permissive = if w.eq_ignore_ascii_case("permissive") {
8825                true
8826            } else if w.eq_ignore_ascii_case("restrictive") {
8827                false
8828            } else {
8829                return Err(self.err(alloc::format!(
8830                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8831                )));
8832            };
8833        }
8834
8835        let mut cmd = PolicyCmd::All;
8836        if matches!(self.peek(), Token::For) {
8837            self.advance();
8838            cmd = self.parse_policy_cmd()?;
8839        }
8840
8841        let mut roles = Vec::new();
8842        if matches!(self.peek(), Token::To) {
8843            self.advance();
8844            roles = self.parse_policy_roles()?;
8845        }
8846
8847        let mut using = None;
8848        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8849        {
8850            self.advance();
8851            using = Some(self.parse_paren_expr("USING")?);
8852        }
8853
8854        let mut with_check = None;
8855        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8856        {
8857            self.advance();
8858            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8859            {
8860                return Err(self.err(alloc::format!(
8861                    "expected CHECK after WITH, got {:?}",
8862                    self.peek()
8863                )));
8864            }
8865            self.advance();
8866            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8867        }
8868
8869        // Clause-per-command matrix (PG wording).
8870        match cmd {
8871            PolicyCmd::Insert => {
8872                if using.is_some() {
8873                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8874                }
8875            }
8876            PolicyCmd::Select | PolicyCmd::Delete => {
8877                if with_check.is_some() {
8878                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8879                }
8880            }
8881            PolicyCmd::Update | PolicyCmd::All => {}
8882        }
8883
8884        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8885            name,
8886            table,
8887            permissive,
8888            cmd,
8889            roles,
8890            using,
8891            with_check,
8892        }))
8893    }
8894
8895    /// v7.39 (RLS) — the command word after `FOR`.
8896    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8897        use crate::ast::PolicyCmd;
8898        match self.peek().clone() {
8899            Token::All => {
8900                self.advance();
8901                Ok(PolicyCmd::All)
8902            }
8903            Token::Select => {
8904                self.advance();
8905                Ok(PolicyCmd::Select)
8906            }
8907            Token::Insert => {
8908                self.advance();
8909                Ok(PolicyCmd::Insert)
8910            }
8911            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8912                self.advance();
8913                Ok(PolicyCmd::Update)
8914            }
8915            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8916                self.advance();
8917                Ok(PolicyCmd::Delete)
8918            }
8919            other => Err(self.err(alloc::format!(
8920                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8921            ))),
8922        }
8923    }
8924
8925    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8926    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8927    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8928        let name = self.expect_ident_like()?;
8929        if !matches!(self.peek(), Token::On) {
8930            return Err(self.err(alloc::format!(
8931                "expected ON after ALTER POLICY name, got {:?}",
8932                self.peek()
8933            )));
8934        }
8935        self.advance();
8936        let table = self.expect_ident_like()?;
8937
8938        // RENAME TO new
8939        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8940        {
8941            self.advance();
8942            if !matches!(self.peek(), Token::To) {
8943                return Err(self.err(alloc::format!(
8944                    "expected TO after RENAME, got {:?}",
8945                    self.peek()
8946                )));
8947            }
8948            self.advance();
8949            let new = self.expect_ident_like()?;
8950            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8951                name,
8952                table,
8953                rename_to: Some(new),
8954                roles: None,
8955                using: None,
8956                with_check: None,
8957            }));
8958        }
8959
8960        let mut roles = None;
8961        if matches!(self.peek(), Token::To) {
8962            self.advance();
8963            roles = Some(self.parse_policy_roles()?);
8964        }
8965        let mut using = None;
8966        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8967        {
8968            self.advance();
8969            using = Some(self.parse_paren_expr("USING")?);
8970        }
8971        let mut with_check = None;
8972        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8973        {
8974            self.advance();
8975            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8976            {
8977                return Err(self.err(alloc::format!(
8978                    "expected CHECK after WITH, got {:?}",
8979                    self.peek()
8980                )));
8981            }
8982            self.advance();
8983            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8984        }
8985        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8986            name,
8987            table,
8988            rename_to: None,
8989            roles,
8990            using,
8991            with_check,
8992        }))
8993    }
8994
8995    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8996    /// `DROP POLICY`.
8997    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8998        let if_exists = self.consume_if_exists();
8999        let name = self.expect_ident_like()?;
9000        if !matches!(self.peek(), Token::On) {
9001            return Err(self.err(alloc::format!(
9002                "expected ON after DROP POLICY name, got {:?}",
9003                self.peek()
9004            )));
9005        }
9006        self.advance();
9007        let table = self.expect_ident_like()?;
9008        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
9009            name,
9010            table,
9011            if_exists,
9012        }))
9013    }
9014}
9015fn wrap_from_leaves(
9016    e: &mut Expr,
9017    names: &[String],
9018    make: &dyn Fn(Expr) -> Expr,
9019    refs: &dyn Fn(&Expr) -> bool,
9020) {
9021    if let Expr::Column(c) = e {
9022        if c.qualifier
9023            .as_deref()
9024            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
9025        {
9026            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
9027            *e = make(taken);
9028        }
9029        return;
9030    }
9031    match e {
9032        Expr::Binary { lhs, rhs, .. } => {
9033            wrap_from_leaves(lhs, names, make, refs);
9034            wrap_from_leaves(rhs, names, make, refs);
9035        }
9036        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
9037            wrap_from_leaves(expr, names, make, refs)
9038        }
9039        Expr::FunctionCall { args, .. } => {
9040            for a in args.iter_mut() {
9041                wrap_from_leaves(a, names, make, refs);
9042            }
9043        }
9044        Expr::Case {
9045            operand,
9046            branches,
9047            else_branch,
9048        } => {
9049            if let Some(o) = operand.as_deref_mut() {
9050                wrap_from_leaves(o, names, make, refs);
9051            }
9052            for (w, t) in branches.iter_mut() {
9053                wrap_from_leaves(w, names, make, refs);
9054                wrap_from_leaves(t, names, make, refs);
9055            }
9056            if let Some(el) = else_branch.as_deref_mut() {
9057                wrap_from_leaves(el, names, make, refs);
9058            }
9059        }
9060        // Compound variants the walk doesn't decompose: keep the
9061        // pre-D.30 behavior — wrap the whole sub-expr if it touches
9062        // a source table, so nothing regresses.
9063        other => {
9064            if refs(other) {
9065                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
9066                *other = make(taken);
9067            }
9068        }
9069    }
9070}
9071
9072/// v7.39 (round 241) — does this expression reference any of the FROM /
9073/// USING table names (shared by the UPDATE…FROM and DELETE…USING
9074/// lowerings)?
9075fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
9076    match e {
9077        Expr::Column(c) => c
9078            .qualifier
9079            .as_deref()
9080            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9081        Expr::Binary { lhs, rhs, .. } => {
9082            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
9083        }
9084        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
9085        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
9086        Expr::Case {
9087            operand,
9088            branches,
9089            else_branch,
9090        } => {
9091            operand
9092                .as_deref()
9093                .is_some_and(|o| expr_refs_tables(o, names))
9094                || branches
9095                    .iter()
9096                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
9097                || else_branch
9098                    .as_deref()
9099                    .is_some_and(|el| expr_refs_tables(el, names))
9100        }
9101        _ => false,
9102    }
9103}
9104
9105impl Parser {
9106    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
9107    /// Caller already consumed the leading `UPDATE` ident.
9108    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
9109    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
9110    /// after the target name has been read. `JOIN` is a reserved token;
9111    /// the qualifiers are bare idents.
9112    fn peek_is_update_join_start(&self) -> bool {
9113        match self.peek() {
9114            // JOIN and its qualifiers are reserved lexer tokens (the grammar
9115            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
9116            Token::Join
9117            | Token::Inner
9118            | Token::Left
9119            | Token::Right
9120            | Token::Cross
9121            | Token::Full => true,
9122            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
9123            Token::Ident(s) | Token::QuotedIdent(s) => {
9124                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
9125            }
9126            _ => false,
9127        }
9128    }
9129
9130    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
9131    /// USER-variable assignment. Its own per-session namespace, an arbitrary
9132    /// expression on the right, and `:=` as a second spelling of `=`.
9133    ///
9134    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
9135    /// from sits on the nesting recursion chain (a CTE body, a subquery),
9136    /// and holding this loop's `Vec` + `String` locals there overflowed the
9137    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
9138    #[inline(never)]
9139    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
9140        let mut assigns: Vec<(String, Expr)> = Vec::new();
9141        let mut settings: Vec<(String, Expr)> = Vec::new();
9142        loop {
9143            // v7.39 (round 554) — a plain NAME here is a session
9144            // setting, not a user variable. mysqldump writes the two in
9145            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
9146            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
9147            // changes it — and this refused the mixture outright, so no
9148            // dump could be restored past its preamble.
9149            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9150                self.advance();
9151                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9152                    return Err(self.err(alloc::format!(
9153                        "expected `=` after {name}, got {:?}",
9154                        self.peek()
9155                    )));
9156                }
9157                self.advance();
9158                let value = self.parse_expr(0)?;
9159                settings.push((name.to_ascii_lowercase(), value));
9160                if matches!(self.peek(), Token::Comma) {
9161                    self.advance();
9162                    continue;
9163                }
9164                break;
9165            }
9166            let Token::SessionVar(raw) = self.peek().clone() else {
9167                return Err(self.err(alloc::format!(
9168                    "expected a user variable after SET, got {:?}",
9169                    self.peek()
9170                )));
9171            };
9172            if raw.starts_with("@@") {
9173                return Err(self.err(alloc::string::String::from(
9174                    "cannot mix `@@` settings with `@` user variables in one SET",
9175                )));
9176            }
9177            self.advance();
9178            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9179                return Err(self.err(alloc::format!(
9180                    "expected `=` or `:=` after {raw}, got {:?}",
9181                    self.peek()
9182                )));
9183            }
9184            self.advance();
9185            let value = self.parse_expr(0)?;
9186            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9187            if matches!(self.peek(), Token::Comma) {
9188                self.advance();
9189                continue;
9190            }
9191            break;
9192        }
9193        Ok(Statement::SetUserVars(assigns, settings))
9194    }
9195
9196    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9197        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9198        // NAMED `only` until now, which failed on `relation "only" does
9199        // not exist`. The lookahead is what keeps a table actually
9200        // called `only` working: the keyword is only a keyword when a
9201        // TABLE NAME follows it — and `SET` arrives as an identifier
9202        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9203        // for the table and die on the `=`. Measured by the pin.
9204        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9205            if s.eq_ignore_ascii_case("only"))
9206            && matches!(
9207                self.tokens.get(self.pos + 1),
9208                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9209            );
9210        if only {
9211            self.advance();
9212        }
9213        let table = self.expect_ident_like()?;
9214        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9215        // bare spelling; a bare identifier that is the SET keyword itself
9216        // is the clause, not an alias.
9217        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9218        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9219        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9220        // following JOIN a syntax error.
9221        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9222        let alias = if matches!(self.peek(), Token::As) {
9223            self.advance();
9224            Some(self.expect_ident_like()?)
9225        } else {
9226            match self.peek() {
9227                Token::Ident(s) | Token::QuotedIdent(s)
9228                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
9229                {
9230                    let a = s.clone();
9231                    self.advance();
9232                    Some(a)
9233                }
9234                _ => None,
9235            }
9236        };
9237        // v7.39 (round 420) — MySQL's multi-table UPDATE:
9238        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
9239        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
9240        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9241        // The FIRST table is the mutation target and the rest are sources —
9242        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9243        // SPG already lowers onto correlated subqueries. So rewind, let
9244        // `parse_from_clause` read the whole list (it handles aliases, comma
9245        // lists, and every JOIN form), then peel the target off the front.
9246        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9247            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9248        {
9249            // NOTE: `advance()` destroys the tokens it returns
9250            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9251            // is NOT possible — the tail is read forward, once, through the
9252            // same grammar `parse_from_clause` uses after its primary.
9253            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9254            let mut joins = self.parse_from_joins(&target_qual)?;
9255            if joins.is_empty() {
9256                return Err(self.err(alloc::string::String::from(
9257                    "multi-table UPDATE needs at least one source table",
9258                )));
9259            }
9260            let head = joins.remove(0);
9261            // A LEFT join keeps every target row (the unmatched ones see NULL
9262            // on the source side), so it must NOT get the EXISTS row filter
9263            // the inner / comma forms use.
9264            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9265            let src = FromClause {
9266                primary: head.table,
9267                joins,
9268            };
9269            (Some(src), head.on, outer)
9270        } else {
9271            (None, None, false)
9272        };
9273        self.expect_keyword_ident("set")?;
9274        let mut assignments = Vec::new();
9275        loop {
9276            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9277            // …)` — the parenthesized multi-assignment. Expressions
9278            // assign positionally; a subquery RHS clones per column
9279            // keeping only the Nth projection item.
9280            if matches!(self.peek(), Token::LParen) {
9281                self.advance();
9282                let mut cols = alloc::vec![self.expect_ident_like()?];
9283                while matches!(self.peek(), Token::Comma) {
9284                    self.advance();
9285                    cols.push(self.expect_ident_like()?);
9286                }
9287                if !matches!(self.peek(), Token::RParen) {
9288                    return Err(self.err(format!(
9289                        "expected ')' after SET column list, got {:?}",
9290                        self.peek()
9291                    )));
9292                }
9293                self.advance();
9294                if !matches!(self.peek(), Token::Eq) {
9295                    return Err(self.err(format!(
9296                        "expected `=` after SET column list, got {:?}",
9297                        self.peek()
9298                    )));
9299                }
9300                self.advance();
9301                if !matches!(self.peek(), Token::LParen) {
9302                    return Err(self.err(format!(
9303                        "expected '(' after SET (…) =, got {:?}",
9304                        self.peek()
9305                    )));
9306                }
9307                self.advance();
9308                if matches!(self.peek(), Token::Select) {
9309                    let inner = match self.parse_select_stmt()? {
9310                        Statement::Select(s) => s,
9311                        other => {
9312                            return Err(self.err(alloc::format!(
9313                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9314                            )));
9315                        }
9316                    };
9317                    if !matches!(self.peek(), Token::RParen) {
9318                        return Err(self.err(format!(
9319                            "expected ')' after SET subquery, got {:?}",
9320                            self.peek()
9321                        )));
9322                    }
9323                    self.advance();
9324                    if inner.items.len() != cols.len() {
9325                        return Err(self.err(alloc::format!(
9326                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9327                            cols.len(),
9328                            inner.items.len()
9329                        )));
9330                    }
9331                    for (i, col) in cols.into_iter().enumerate() {
9332                        let mut sub = inner.clone();
9333                        sub.items = alloc::vec![sub.items[i].clone()];
9334                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9335                    }
9336                } else {
9337                    let mut exprs = alloc::vec![self.parse_expr(0)?];
9338                    while matches!(self.peek(), Token::Comma) {
9339                        self.advance();
9340                        exprs.push(self.parse_expr(0)?);
9341                    }
9342                    if !matches!(self.peek(), Token::RParen) {
9343                        return Err(self.err(format!(
9344                            "expected ')' after SET row values, got {:?}",
9345                            self.peek()
9346                        )));
9347                    }
9348                    self.advance();
9349                    if exprs.len() != cols.len() {
9350                        return Err(self.err(alloc::format!(
9351                            "SET (…) = (…) arity mismatch: {} columns, {} values",
9352                            cols.len(),
9353                            exprs.len()
9354                        )));
9355                    }
9356                    for (col, e) in cols.into_iter().zip(exprs) {
9357                        assignments.push((col, e));
9358                    }
9359                }
9360                if matches!(self.peek(), Token::Comma) {
9361                    self.advance();
9362                    continue;
9363                }
9364                break;
9365            }
9366            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9367            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9368            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9369            // `public.` dump qualifiers), so the qualifier has to be read off
9370            // the token stream first — otherwise `SET b.v = 888` would write
9371            // to the TARGET table's `v` while naming a source table, a
9372            // silent-wrong. A qualifier naming a SOURCE table means a
9373            // multi-TARGET update — mutating two tables in one statement —
9374            // which SPG does not model, so it is refused loudly.
9375            let set_qual: Option<String> = if mysql_from.is_some()
9376                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9377            {
9378                match self.peek() {
9379                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9380                    _ => None,
9381                }
9382            } else {
9383                None
9384            };
9385            let col = self.expect_ident_like()?;
9386            if let Some(q) = set_qual {
9387                let names_target = q.eq_ignore_ascii_case(&table)
9388                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9389                if !names_target {
9390                    return Err(self.err(alloc::format!(
9391                        "multi-table UPDATE can only assign to its first table \
9392                         ({table}); `{q}.{col}` targets another table"
9393                    )));
9394                }
9395            }
9396            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9397            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9398            // `__column_default` marker lowering just below). PG assigns to the
9399            // i-th (1-based) element, NULL-padding when i exceeds the length.
9400            if matches!(self.peek(), Token::LBracket) {
9401                self.advance();
9402                let index = self.parse_expr(0)?;
9403                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9404                // (and the open `arr[lo:]`), lowered to
9405                // `__array_assign_slice`. Only the single-subscript form
9406                // parsed before, so a slice assignment was a syntax error.
9407                let mut slice_hi: Option<Option<Expr>> = None;
9408                if matches!(self.peek(), Token::Colon) {
9409                    self.advance();
9410                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9411                        None
9412                    } else {
9413                        Some(self.parse_expr(0)?)
9414                    });
9415                }
9416                if !matches!(self.peek(), Token::RBracket) {
9417                    return Err(self.err(format!(
9418                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9419                        self.peek()
9420                    )));
9421                }
9422                self.advance();
9423                if !matches!(self.peek(), Token::Eq) {
9424                    return Err(self.err(format!(
9425                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9426                        self.peek()
9427                    )));
9428                }
9429                self.advance();
9430                let value = self.parse_expr(0)?;
9431                // PG merges several subscript writes to the same column into one
9432                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9433                // assignment to `col` rather than each overwriting the original.
9434                let existing = assignments.iter().position(|(c, _)| c == &col);
9435                let base = match existing {
9436                    Some(i) => assignments[i].1.clone(),
9437                    None => Expr::Column(ColumnName {
9438                        qualifier: None,
9439                        name: col.clone(),
9440                    }),
9441                };
9442                let assigned = match slice_hi {
9443                    None => Expr::FunctionCall {
9444                        name: "__array_assign".to_string(),
9445                        args: alloc::vec![base, index, value],
9446                    },
9447                    Some(hi) => Expr::FunctionCall {
9448                        name: "__array_assign_slice".to_string(),
9449                        args: alloc::vec![
9450                            base,
9451                            index,
9452                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9453                            value,
9454                        ],
9455                    },
9456                };
9457                match existing {
9458                    Some(i) => assignments[i].1 = assigned,
9459                    None => assignments.push((col, assigned)),
9460                }
9461                if matches!(self.peek(), Token::Comma) {
9462                    self.advance();
9463                    continue;
9464                }
9465                break;
9466            }
9467            if !matches!(self.peek(), Token::Eq) {
9468                return Err(self.err(format!(
9469                    "expected `=` after column name in UPDATE SET, got {:?}",
9470                    self.peek()
9471                )));
9472            }
9473            self.advance();
9474            // `SET col = DEFAULT` — the column's declared default;
9475            // rides out as a marker call the update executor
9476            // resolves against the schema.
9477            let value = if matches!(self.peek(), Token::Default) {
9478                self.advance();
9479                Expr::FunctionCall {
9480                    name: "__column_default".to_string(),
9481                    args: Vec::new(),
9482                }
9483            } else {
9484                self.parse_expr(0)?
9485            };
9486            assignments.push((col, value));
9487            if matches!(self.peek(), Token::Comma) {
9488                self.advance();
9489                continue;
9490            }
9491            break;
9492        }
9493        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9494        // update. Lowers onto the correlated-subquery machinery:
9495        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9496        // and each assignment that references a FROM-list table
9497        // wraps into a correlated scalar subquery
9498        // (SELECT expr FROM src WHERE cond). Equivalent for the
9499        // unique-join shape (the overwhelmingly common one); a
9500        // multi-match, which PG resolves by arbitrary pick,
9501        // surfaces as a scalar-subquery cardinality error instead
9502        // of a silent arbitrary result.
9503        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9504        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9505        // the SAME lowering below. Both spellings together is not legal in
9506        // either dialect.
9507        let from_clause = if let Some(fc) = mysql_from {
9508            if matches!(self.peek(), Token::From) {
9509                return Err(self.err(alloc::string::String::from(
9510                    "multi-table UPDATE already names its sources; drop the FROM clause",
9511                )));
9512            }
9513            Some(fc)
9514        } else if matches!(self.peek(), Token::From) {
9515            self.advance();
9516            Some(self.parse_from_clause()?)
9517        } else {
9518            None
9519        };
9520        let where_ = if matches!(self.peek(), Token::Where) {
9521            self.advance();
9522            Some(self.parse_expr(0)?)
9523        } else {
9524            None
9525        };
9526        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9527        // and the TARGET-row filter are NOT the same predicate once a LEFT
9528        // join is involved:
9529        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9530        //     one conjunction, and the whole thing filters target rows via
9531        //     EXISTS.
9532        //   * LEFT join: only the ON predicate belongs inside the source
9533        //     subquery. The WHERE still filters TARGET rows (with source
9534        //     columns read through the correlated subquery, which yields NULL
9535        //     for an unmatched row — exactly LEFT-join semantics).
9536        // Round 420 folded ON into WHERE unconditionally and then dropped the
9537        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9538        // WHERE a.id > 1` updated EVERY row.
9539        let sub_where = match (mysql_on.clone(), where_.clone()) {
9540            _ if mysql_outer => mysql_on.clone(),
9541            (Some(on), Some(w)) => Some(Expr::Binary {
9542                lhs: Box::new(on),
9543                op: crate::ast::BinOp::And,
9544                rhs: Box::new(w),
9545            }),
9546            (Some(on), None) => Some(on),
9547            (None, w) => w,
9548        };
9549        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9550        // has no such clause on UPDATE, so this is accepted only under the
9551        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9552        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9553        let mut returning = self.parse_optional_returning()?;
9554        // v7.39 (round 533) — kept for the engine, which can resolve the
9555        // UNQUALIFIED leaves this lowering has to leave alone.
9556        let from_sources = from_clause.as_ref().map(|fc| {
9557            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9558                from: fc.clone(),
9559                sub_where: sub_where.clone(),
9560            })
9561        });
9562        let (assignments, where_) = if let Some(fc) = from_clause {
9563            let names: Vec<String> = core::iter::once(&fc.primary)
9564                .chain(fc.joins.iter().map(|j| &j.table))
9565                .flat_map(|t| {
9566                    t.alias
9567                        .clone()
9568                        .into_iter()
9569                        .chain(core::iter::once(t.name.clone()))
9570                })
9571                .collect();
9572            let refs_list = |e: &Expr| -> bool {
9573                fn walk(e: &Expr, names: &[String]) -> bool {
9574                    match e {
9575                        Expr::Column(c) => c
9576                            .qualifier
9577                            .as_deref()
9578                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9579                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9580                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9581                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9582                        Expr::Case {
9583                            operand,
9584                            branches,
9585                            else_branch,
9586                        } => {
9587                            operand.as_deref().is_some_and(|o| walk(o, names))
9588                                || branches
9589                                    .iter()
9590                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9591                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9592                        }
9593                        _ => false,
9594                    }
9595                }
9596                walk(e, &names)
9597            };
9598            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9599                locking: None,
9600                ctes: Vec::new(),
9601                distinct: false,
9602                distinct_on: Vec::new(),
9603                items,
9604                from: Some(fc.clone()),
9605                where_: sub_where.clone(),
9606                group_by: None,
9607                group_by_all: false,
9608                having: None,
9609                unions: Vec::new(),
9610                order_by: Vec::new(),
9611                limit: None,
9612                offset: None,
9613                limit_with_ties: false,
9614                window_check_exprs: Vec::new(),
9615            };
9616            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9617            // assignment RHS with a correlated scalar subquery, instead of
9618            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9619            // column reference (`SET v = v + u.bonus`, where `v` is the target
9620            // table's column) inside a subquery whose FROM only has the source
9621            // table, so the unqualified `v` resolved against the source and
9622            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9623            // context — where they belong — fixes it; only the source columns
9624            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9625            // compound variants the leaf-walk doesn't decompose.
9626            let make_subq = |inner: Expr| {
9627                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9628                    expr: inner,
9629                    alias: None,
9630                }])))
9631            };
9632            let assignments = assignments
9633                .into_iter()
9634                .map(|(col, mut expr)| {
9635                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9636                    (col, expr)
9637                })
9638                .collect();
9639            let exists = Expr::Exists {
9640                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9641                    expr: Expr::Literal(Literal::Integer(1)),
9642                    alias: None,
9643                }])),
9644                negated: false,
9645            };
9646            // v7.39 (round 241) — RETURNING may reference the FROM-list
9647            // tables too (`RETURNING emp.id, dept.name`); the same
9648            // leaf-to-correlated-subquery lowering the assignments get.
9649            // Without it the qualifier died at eval with "unknown table
9650            // qualifier". (RETURNING was parsed before this block — the
9651            // lowering is a pure AST transformation.)
9652            if let Some(items) = returning.as_mut() {
9653                for item in items.iter_mut() {
9654                    if let SelectItem::Expr { expr, .. } = item {
9655                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9656                    }
9657                }
9658            }
9659            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9660            // EVERY matching target row: it gets no EXISTS filter, but the
9661            // caller's WHERE still applies, with source columns read through
9662            // the correlated subquery (NULL when unmatched — LEFT-join
9663            // semantics). `sub_where` above already excluded the WHERE from
9664            // the source subquery for this case.
9665            if mysql_outer {
9666                let mut outer = where_;
9667                if let Some(w) = outer.as_mut() {
9668                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9669                }
9670                (assignments, outer)
9671            } else {
9672                (assignments, Some(exists))
9673            }
9674        } else {
9675            (assignments, where_)
9676        };
9677        Ok(Statement::Update(crate::ast::UpdateStatement {
9678            ctes: Vec::new(),
9679            table,
9680            only,
9681            alias,
9682            assignments,
9683            from_sources,
9684            where_,
9685            order_limit: update_order_limit,
9686            returning,
9687        }))
9688    }
9689
9690    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9691    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9692    /// clause and its meaning are identical, so both call this rather than
9693    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9694    /// legal. PG has no such clause on either statement, so it is read only
9695    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9696    /// errors.
9697    ///
9698    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9699    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9700    /// stack in round 430.
9701    #[inline(never)]
9702    fn parse_mysql_dml_order_limit(
9703        &mut self,
9704        what: &str,
9705    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9706        if !self.mysql_dialect {
9707            return Ok(None);
9708        }
9709        let order_by = self.parse_order_by_keys()?;
9710        let limit = if matches!(self.peek(), Token::Limit) {
9711            self.advance();
9712            let tok = self.advance();
9713            let Token::Integer(n) = tok else {
9714                return Err(self.err(alloc::format!(
9715                    "expected integer after {what} LIMIT, got {tok:?}"
9716                )));
9717            };
9718            // MySQL rejects the `LIMIT offset, count` form here — only a
9719            // single row count is legal on a DML statement.
9720            if matches!(self.peek(), Token::Comma) {
9721                return Err(self.err(alloc::format!(
9722                    "{what} LIMIT takes a row count, not an offset"
9723                )));
9724            }
9725            let n = u32::try_from(n)
9726                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9727            Some(n)
9728        } else {
9729            None
9730        };
9731        if order_by.is_empty() && limit.is_none() {
9732            return Ok(None);
9733        }
9734        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9735            order_by,
9736            limit,
9737        })))
9738    }
9739
9740    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9741    /// the leading `DELETE` ident.
9742    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9743        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9744        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9745        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9746        // parse here; it reaches the existing USING path with the target
9747        // repeated in the list, which the source-list peel below handles.)
9748        // More than one name is a multi-TARGET delete, which SPG does not
9749        // model; it is refused rather than half-applied.
9750        let mysql_pre_target: Option<String> =
9751            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9752                let first = self.expect_ident_like()?;
9753                if matches!(self.peek(), Token::Comma) {
9754                    return Err(self.err(alloc::format!(
9755                        "multi-table DELETE can only delete from one table; \
9756                     `DELETE {first}, …` names several"
9757                    )));
9758                }
9759                Some(first)
9760            } else {
9761                None
9762            };
9763        if !matches!(self.peek(), Token::From) {
9764            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9765        }
9766        self.advance();
9767        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9768        // lookahead as the UPDATE spelling.
9769        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9770            if s.eq_ignore_ascii_case("only"))
9771            && matches!(
9772                self.tokens.get(self.pos + 1),
9773                Some(Token::Ident(_) | Token::QuotedIdent(_))
9774            );
9775        if only {
9776            self.advance();
9777        }
9778        let table = self.expect_ident_like()?;
9779        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9780        // spelling must not swallow the clause keywords that can follow
9781        // the target.
9782        let alias = if matches!(self.peek(), Token::As) {
9783            self.advance();
9784            Some(self.expect_ident_like()?)
9785        } else {
9786            match self.peek() {
9787                Token::Ident(s) | Token::QuotedIdent(s)
9788                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9789                {
9790                    let a = s.clone();
9791                    self.advance();
9792                    Some(a)
9793                }
9794                _ => None,
9795            }
9796        };
9797        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9798        // through the SAME join grammar the FROM clause uses (see the
9799        // `advance()`-destroys-tokens note on `parse_from_joins`).
9800        let mut mysql_on: Option<Expr> = None;
9801        let mut mysql_outer = false;
9802        let mysql_using = if mysql_pre_target.is_some()
9803            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9804        {
9805            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9806            let mut joins = self.parse_from_joins(&target_qual)?;
9807            if joins.is_empty() {
9808                return Err(self.err(alloc::string::String::from(
9809                    "multi-table DELETE needs at least one source table",
9810                )));
9811            }
9812            let head = joins.remove(0);
9813            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9814            mysql_on = head.on;
9815            Some(FromClause {
9816                primary: head.table,
9817                joins,
9818            })
9819        } else {
9820            None
9821        };
9822        // The pre-FROM target must be the table the FROM names (or its
9823        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9824        // is not the scan target.
9825        if let Some(t) = &mysql_pre_target {
9826            let names_target = t.eq_ignore_ascii_case(&table)
9827                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9828            if !names_target {
9829                return Err(self.err(alloc::format!(
9830                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9831                )));
9832            }
9833        }
9834        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9835        // delete. Same lowering as UPDATE … FROM: the WHERE
9836        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9837        // target row by the correlated machinery.
9838        let using_clause = if let Some(fc) = mysql_using {
9839            Some(fc)
9840        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9841            self.advance();
9842            let mut fc = self.parse_from_clause()?;
9843            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9844            // repeats the TARGET as the first USING entry (PG's spelling
9845            // lists only the extra sources). Peel it so the source subquery
9846            // does not re-scan — and shadow — the target table.
9847            let primary_is_target =
9848                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9849            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9850                let head = fc.joins.remove(0);
9851                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9852                mysql_on = head.on;
9853                fc = FromClause {
9854                    primary: head.table,
9855                    joins: fc.joins,
9856                };
9857            }
9858            Some(fc)
9859        } else {
9860            None
9861        };
9862        let where_ = if matches!(self.peek(), Token::Where) {
9863            self.advance();
9864            Some(self.parse_expr(0)?)
9865        } else {
9866            None
9867        };
9868        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9869        // read before RETURNING (MariaDB's own extension trails the LIMIT).
9870        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9871        let mut returning = self.parse_optional_returning()?;
9872        let where_ = if let Some(fc) = using_clause {
9873            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9874            // a USING-table reference in RETURNING becomes a correlated
9875            // scalar subquery over the USING list.
9876            let names: Vec<String> = core::iter::once(&fc.primary)
9877                .chain(fc.joins.iter().map(|j| &j.table))
9878                .flat_map(|t| {
9879                    t.alias
9880                        .clone()
9881                        .into_iter()
9882                        .chain(core::iter::once(t.name.clone()))
9883                })
9884                .collect();
9885            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9886            // join filters the SOURCE subquery on the ON predicate alone and
9887            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9888            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9889            // rows); every other form folds ON and WHERE into one EXISTS.
9890            let sub_where = match (mysql_on.clone(), where_.clone()) {
9891                _ if mysql_outer => mysql_on.clone(),
9892                (Some(on), Some(w)) => Some(Expr::Binary {
9893                    lhs: Box::new(on),
9894                    op: crate::ast::BinOp::And,
9895                    rhs: Box::new(w),
9896                }),
9897                (Some(on), None) => Some(on),
9898                (None, w) => w,
9899            };
9900            let exists_where = sub_where.clone();
9901            let sub_fc = fc.clone();
9902            let make_subq = move |leaf: Expr| -> Expr {
9903                Expr::ScalarSubquery(Box::new(SelectStatement {
9904                    locking: None,
9905                    ctes: Vec::new(),
9906                    distinct: false,
9907                    distinct_on: Vec::new(),
9908                    items: alloc::vec![SelectItem::Expr {
9909                        expr: leaf,
9910                        alias: None,
9911                    }],
9912                    from: Some(sub_fc.clone()),
9913                    where_: sub_where.clone(),
9914                    group_by: None,
9915                    group_by_all: false,
9916                    having: None,
9917                    unions: Vec::new(),
9918                    order_by: Vec::new(),
9919                    limit: None,
9920                    offset: None,
9921                    limit_with_ties: false,
9922                    window_check_exprs: Vec::new(),
9923                }))
9924            };
9925            let refs = |e: &Expr| expr_refs_tables(e, &names);
9926            if let Some(items) = returning.as_mut() {
9927                for item in items.iter_mut() {
9928                    if let SelectItem::Expr { expr, .. } = item {
9929                        wrap_from_leaves(expr, &names, &make_subq, &refs);
9930                    }
9931                }
9932            }
9933            // A LEFT join deletes the target rows the WHERE selects, reading
9934            // source columns through the correlated subquery (NULL when
9935            // unmatched); no EXISTS row filter.
9936            if mysql_outer {
9937                let mut outer = where_;
9938                if let Some(w) = outer.as_mut() {
9939                    wrap_from_leaves(w, &names, &make_subq, &refs);
9940                }
9941                outer
9942            } else {
9943                Some(Expr::Exists {
9944                    subquery: Box::new(SelectStatement {
9945                        locking: None,
9946                        ctes: Vec::new(),
9947                        distinct: false,
9948                        distinct_on: Vec::new(),
9949                        items: alloc::vec![SelectItem::Expr {
9950                            expr: Expr::Literal(Literal::Integer(1)),
9951                            alias: None,
9952                        }],
9953                        from: Some(fc),
9954                        where_: exists_where,
9955                        group_by: None,
9956                        group_by_all: false,
9957                        having: None,
9958                        unions: Vec::new(),
9959                        order_by: Vec::new(),
9960                        limit: None,
9961                        offset: None,
9962                        limit_with_ties: false,
9963                        window_check_exprs: Vec::new(),
9964                    }),
9965                    negated: false,
9966                })
9967            }
9968        } else {
9969            where_
9970        };
9971        Ok(Statement::Delete(crate::ast::DeleteStatement {
9972            ctes: Vec::new(),
9973            table,
9974            only,
9975            alias,
9976            where_,
9977            order_limit: delete_order_limit,
9978            returning,
9979        }))
9980    }
9981
9982    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9983    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9984    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9985    /// keyword. v7.17 surface:
9986    ///   * source: table reference (subquery source is a follow-up)
9987    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9988    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9989    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
9990    ///     order
9991    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9992        // INTO
9993        let is_into_kw = matches!(self.peek(), Token::Into)
9994            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9995        if !is_into_kw {
9996            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9997        }
9998        self.advance();
9999        let target = self.expect_ident_like()?;
10000        // Optional alias — bare ident before USING.
10001        let target_alias = match self.peek() {
10002            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
10003                Some(self.expect_ident_like()?)
10004            }
10005            _ => None,
10006        };
10007        // USING
10008        let is_using_kw = matches!(
10009            self.peek(),
10010            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
10011        );
10012        if !is_using_kw {
10013            return Err(self.err(format!(
10014                "expected USING after MERGE INTO target, got {:?}",
10015                self.peek()
10016            )));
10017        }
10018        self.advance();
10019        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
10020        // <table> [alias]`. PG requires an alias after a subquery source.
10021        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
10022            self.advance(); // (
10023            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
10024            // constant-SELECT lowering the derived-table parser uses
10025            // (PG deletes through this form; it was a parse error).
10026            let inner = if matches!(self.peek(), Token::Values) {
10027                self.advance(); // VALUES
10028                Statement::Select(self.parse_values_rows_body()?)
10029            } else {
10030                self.parse_select_stmt()?
10031            };
10032            match self.advance() {
10033                Token::RParen => {}
10034                other => {
10035                    return Err(self.err(format!(
10036                        "expected ')' after MERGE USING subquery, got {other:?}"
10037                    )));
10038                }
10039            }
10040            let Statement::Select(sub) = inner else {
10041                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
10042            };
10043            (String::new(), Some(Box::new(sub)))
10044        } else {
10045            (self.expect_ident_like()?, None)
10046        };
10047        let source_alias = match self.peek() {
10048            Token::Ident(s) | Token::QuotedIdent(s)
10049                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
10050            {
10051                Some(self.expect_ident_like()?)
10052            }
10053            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
10054                self.advance(); // AS
10055                Some(self.expect_ident_like()?)
10056            }
10057            _ => None,
10058        };
10059        // v7.39 (round 768, F31-D5) — optional positional column-alias
10060        // list after the source alias (`s(id, v)`).
10061        let mut source_column_aliases: Vec<String> = Vec::new();
10062        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
10063            self.advance();
10064            loop {
10065                source_column_aliases.push(self.expect_ident_like()?);
10066                match self.peek() {
10067                    Token::Comma => {
10068                        self.advance();
10069                    }
10070                    Token::RParen => {
10071                        self.advance();
10072                        break;
10073                    }
10074                    other => {
10075                        return Err(self.err(format!(
10076                            "expected ',' or ')' in MERGE source column list, got {other:?}"
10077                        )));
10078                    }
10079                }
10080            }
10081        }
10082        if source_select.is_some() && source_alias.is_none() {
10083            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
10084        }
10085        // ON
10086        if !matches!(self.peek(), Token::On) {
10087            return Err(self.err(format!(
10088                "expected ON after MERGE … USING source, got {:?}",
10089                self.peek()
10090            )));
10091        }
10092        self.advance();
10093        let on = self.parse_expr(0)?;
10094        // One or more WHEN clauses.
10095        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
10096        loop {
10097            let is_when_kw = matches!(
10098                self.peek(),
10099                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
10100            );
10101            if !is_when_kw {
10102                break;
10103            }
10104            self.advance(); // WHEN
10105            // [NOT] MATCHED
10106            let matched = if matches!(self.peek(), Token::Not) {
10107                self.advance();
10108                crate::ast::MergeMatched::NotMatched
10109            } else {
10110                crate::ast::MergeMatched::Matched
10111            };
10112            let is_matched_kw = matches!(
10113                self.peek(),
10114                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
10115            );
10116            if !is_matched_kw {
10117                return Err(self.err(format!(
10118                    "expected MATCHED in WHEN clause, got {:?}",
10119                    self.peek()
10120                )));
10121            }
10122            self.advance();
10123            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
10124            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
10125            // to fire for target rows no source row matches.
10126            let mut matched = matched;
10127            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
10128                self.advance();
10129                match self.peek() {
10130                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
10131                        self.advance();
10132                        matched = crate::ast::MergeMatched::NotMatchedBySource;
10133                    }
10134                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
10135                        self.advance();
10136                    }
10137                    other => {
10138                        return Err(self.err(format!(
10139                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
10140                        )));
10141                    }
10142                }
10143            }
10144            // Optional AND <expr>
10145            let condition = if matches!(self.peek(), Token::And) {
10146                self.advance();
10147                Some(self.parse_expr(0)?)
10148            } else {
10149                None
10150            };
10151            // THEN
10152            let is_then_kw = matches!(
10153                self.peek(),
10154                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10155            );
10156            if !is_then_kw {
10157                return Err(self.err(format!(
10158                    "expected THEN in WHEN clause, got {:?}",
10159                    self.peek()
10160                )));
10161            }
10162            self.advance();
10163            // Action: INSERT / UPDATE / DELETE / DO NOTHING
10164            let action = match self.peek().clone() {
10165                Token::Insert => {
10166                    self.advance();
10167                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10168                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10169                    // VALUES (…)` omits it and fills every column in declaration
10170                    // order. PG accepts this; SPG used to require the list.
10171                    let mut columns: Vec<String> = Vec::new();
10172                    if matches!(self.peek(), Token::LParen) {
10173                        self.advance();
10174                        loop {
10175                            columns.push(self.expect_ident_like()?);
10176                            if matches!(self.peek(), Token::Comma) {
10177                                self.advance();
10178                                continue;
10179                            }
10180                            break;
10181                        }
10182                        if !matches!(self.peek(), Token::RParen) {
10183                            return Err(self.err(format!(
10184                                "expected ')' after INSERT column list, got {:?}",
10185                                self.peek()
10186                            )));
10187                        }
10188                        self.advance();
10189                    }
10190                    // VALUES (...)
10191                    if !matches!(self.peek(), Token::Values) {
10192                        return Err(self.err(format!(
10193                            "expected VALUES in MERGE INSERT, got {:?}",
10194                            self.peek()
10195                        )));
10196                    }
10197                    self.advance();
10198                    if !matches!(self.peek(), Token::LParen) {
10199                        return Err(self.err(format!(
10200                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
10201                            self.peek()
10202                        )));
10203                    }
10204                    self.advance();
10205                    let mut values: Vec<crate::ast::Expr> = Vec::new();
10206                    loop {
10207                        values.push(self.parse_expr(0)?);
10208                        if matches!(self.peek(), Token::Comma) {
10209                            self.advance();
10210                            continue;
10211                        }
10212                        break;
10213                    }
10214                    if !matches!(self.peek(), Token::RParen) {
10215                        return Err(self.err(format!(
10216                            "expected ')' after MERGE INSERT values, got {:?}",
10217                            self.peek()
10218                        )));
10219                    }
10220                    self.advance();
10221                    // Empty column list = positional into every column, so the
10222                    // count is checked against the table arity at execution.
10223                    if !columns.is_empty() && columns.len() != values.len() {
10224                        return Err(self.err(format!(
10225                            "MERGE INSERT column count ({}) ≠ value count ({})",
10226                            columns.len(),
10227                            values.len()
10228                        )));
10229                    }
10230                    crate::ast::MergeAction::Insert { columns, values }
10231                }
10232                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10233                    self.advance();
10234                    // SET
10235                    let is_set_kw = matches!(
10236                        self.peek(),
10237                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10238                    );
10239                    if !is_set_kw {
10240                        return Err(self.err(format!(
10241                            "expected SET after UPDATE in MERGE, got {:?}",
10242                            self.peek()
10243                        )));
10244                    }
10245                    self.advance();
10246                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10247                    loop {
10248                        let col = self.expect_ident_like()?;
10249                        if !matches!(self.peek(), Token::Eq) {
10250                            return Err(self.err(format!(
10251                                "expected '=' in MERGE UPDATE assignment, got {:?}",
10252                                self.peek()
10253                            )));
10254                        }
10255                        self.advance();
10256                        let expr = self.parse_expr(0)?;
10257                        assignments.push((col, expr));
10258                        if matches!(self.peek(), Token::Comma) {
10259                            self.advance();
10260                            continue;
10261                        }
10262                        break;
10263                    }
10264                    crate::ast::MergeAction::Update { assignments }
10265                }
10266                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10267                    self.advance();
10268                    crate::ast::MergeAction::Delete
10269                }
10270                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10271                    self.advance();
10272                    let is_nothing_kw = matches!(
10273                        self.peek(),
10274                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10275                    );
10276                    if !is_nothing_kw {
10277                        return Err(self.err(format!(
10278                            "expected NOTHING after DO in MERGE clause, got {:?}",
10279                            self.peek()
10280                        )));
10281                    }
10282                    self.advance();
10283                    crate::ast::MergeAction::DoNothing
10284                }
10285                other => {
10286                    return Err(self.err(format!(
10287                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10288                    )));
10289                }
10290            };
10291            // PG's grammar simply has no INSERT production under BY SOURCE
10292            // (a target row already exists there) — same syntax error.
10293            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10294                && matches!(action, crate::ast::MergeAction::Insert { .. })
10295            {
10296                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10297            }
10298            clauses.push(crate::ast::MergeWhenClause {
10299                matched,
10300                condition,
10301                action,
10302            });
10303        }
10304        if clauses.is_empty() {
10305            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10306        }
10307        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10308        // unconditional (no `AND`) WHEN of the same match kind: it could
10309        // never fire. Check per match kind in clause order.
10310        let mut seen_unconditional_matched = false;
10311        let mut seen_unconditional_not_matched = false;
10312        let mut seen_unconditional_by_source = false;
10313        for c in &clauses {
10314            let seen = match c.matched {
10315                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10316                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10317                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10318            };
10319            if *seen {
10320                return Err(self.err(String::from(
10321                    "unreachable WHEN clause specified after unconditional WHEN clause",
10322                )));
10323            }
10324            if c.condition.is_none() {
10325                *seen = true;
10326            }
10327        }
10328        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10329        let returning = self.parse_optional_returning()?;
10330        Ok(Statement::Merge(crate::ast::MergeStatement {
10331            // Attached by `parse_with_cte_then_select` when the MERGE
10332            // heads a WITH clause (round 149).
10333            ctes: Vec::new(),
10334            target,
10335            target_alias,
10336            source,
10337            source_alias,
10338            source_select,
10339            source_column_aliases,
10340            on,
10341            clauses,
10342            returning,
10343        }))
10344    }
10345
10346    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10347    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10348    /// as SELECT, so `RETURNING *`, `RETURNING col`,
10349    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10350    fn parse_optional_returning(
10351        &mut self,
10352    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10353        let is_returning_kw = matches!(
10354            self.peek(),
10355            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10356        );
10357        if !is_returning_kw {
10358            return Ok(None);
10359        }
10360        self.advance();
10361        let mut items = Vec::new();
10362        loop {
10363            items.push(self.parse_select_item()?);
10364            if matches!(self.peek(), Token::Comma) {
10365                self.advance();
10366                continue;
10367            }
10368            break;
10369        }
10370        Ok(Some(items))
10371    }
10372
10373    /// v6.0.4 — parse the tail of an ALTER statement after the
10374    /// leading `ALTER` keyword has been consumed. Only one form is
10375    /// supported in v6.0.4:
10376    ///
10377    /// ```text
10378    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10379    /// ```
10380    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10381        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10382        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10383        // exclusion) is accepted by stripping the `ONLY` keyword
10384        // before the table parse.
10385        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10386        // and the long PG-dump tail are accepted as no-ops.
10387        match self.advance() {
10388            Token::Index => {}
10389            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10390            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10391            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10392            Token::Table => {
10393                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10394                    self.advance();
10395                }
10396                return self.parse_alter_table_after_keyword();
10397            }
10398            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10399                return self.parse_alter_policy_after_keyword();
10400            }
10401            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10402                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10403                    self.advance();
10404                }
10405                return self.parse_alter_table_after_keyword();
10406            }
10407            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10408            // of the silent-noop tail.
10409            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10410                return self.parse_alter_sequence_after_keyword();
10411            }
10412            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10413            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10414            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10415            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10416                // NB: the match arm consumed `TYPE` via self.advance(); the
10417                // cursor is now at the type name — do NOT advance again.
10418                let type_name = self.expect_ident_like()?;
10419                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10420                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10421                if is_add_value {
10422                    self.advance(); // ADD
10423                    self.advance(); // VALUE
10424                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10425                    // IF/EXISTS as identifiers.
10426                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10427                    {
10428                        let n1 = self.tokens.get(self.pos + 1);
10429                        let n2 = self.tokens.get(self.pos + 2);
10430                        if matches!(n1, Some(Token::Not))
10431                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10432                        {
10433                            self.advance();
10434                            self.advance();
10435                            self.advance();
10436                            true
10437                        } else {
10438                            false
10439                        }
10440                    } else {
10441                        false
10442                    };
10443                    let label = self.expect_string_literal()?;
10444                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10445                    {
10446                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10447                        self.advance();
10448                        let anchor = self.expect_string_literal()?;
10449                        Some((is_before, anchor))
10450                    } else {
10451                        None
10452                    };
10453                    return Ok(Statement::AlterTypeAddValue {
10454                        type_name,
10455                        label,
10456                        if_not_exists,
10457                        position,
10458                    });
10459                }
10460                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10461                // Used to fall into the no-op tail below: accepted, silently
10462                // ignored. `RENAME TO <newtype>` keeps falling through.
10463                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10464                    && matches!(
10465                        self.tokens.get(self.pos + 1),
10466                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10467                    )
10468                {
10469                    self.advance(); // RENAME
10470                    self.advance(); // VALUE
10471                    let old = self.expect_string_literal()?;
10472                    if matches!(self.peek(), Token::To) {
10473                        self.advance();
10474                    } else {
10475                        self.expect_keyword_ident("to")?;
10476                    }
10477                    let new = self.expect_string_literal()?;
10478                    return Ok(Statement::AlterTypeRenameValue {
10479                        type_name,
10480                        old,
10481                        new,
10482                    });
10483                }
10484                // Other ALTER TYPE forms — the ACTION stays a no-op
10485                // (pg_dump tail), but v7.39 (round 708) the NAME is
10486                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10487                // success for a type that does not exist.
10488                self.consume_until_statement_boundary();
10489                return Ok(Statement::ValidateOnly {
10490                    kind: crate::ast::ValidateOnlyKind::TypeName,
10491                    names: alloc::vec![type_name],
10492                });
10493            }
10494            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10495            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10496            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10497            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10498            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10499            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10500            // pg_dump no-op list below: every form used to report success
10501            // and change nothing, which is worse than refusing outright
10502            // (a migration dropping a constraint kept rejecting data).
10503            // NOTE: the enclosing `match self.advance()` already consumed
10504            // the DOMAIN keyword, so the name is next.
10505            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10506                return self.parse_alter_domain_after_keyword();
10507            }
10508            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10509            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10510            // used to fall into the pg_dump no-op tail below, so a DBA
10511            // setting a per-role default was told it worked and nothing
10512            // happened. Intercepted here, BEFORE that tail.
10513            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10514            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10515            // interception below exists: swallowed with the no-op tail, an
10516            // unknown parameter name was ACCEPTED where PG18 answers
10517            // `unrecognized configuration parameter`. SPG applies nothing
10518            // either way — there is no postgresql.auto.conf — but it now
10519            // says so about a name it does not know.
10520            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10521                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10522                // already consumed here. An extra advance eats the SET and
10523                // the parameter name is never seen — which is exactly the
10524                // bug a panic in this branch disproved: the branch WAS on
10525                // the path, the reading of it was wrong.
10526                let mut parameter = None;
10527                // SET <name> … | RESET <name> | RESET ALL
10528                if matches!(self.peek(), Token::Ident(k)
10529                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10530                {
10531                    self.advance();
10532                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10533                        && !n.eq_ignore_ascii_case("all")
10534                    {
10535                        self.advance();
10536                        // A dotted GUC (`plpgsql.check_asserts`) is two
10537                        // tokens; keep the whole name.
10538                        let mut full = n;
10539                        while matches!(self.peek(), Token::Dot) {
10540                            self.advance();
10541                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10542                                full.push('.');
10543                                full.push_str(&t);
10544                            }
10545                        }
10546                        parameter = Some(full);
10547                    }
10548                }
10549                self.consume_until_statement_boundary();
10550                return Ok(Statement::AlterSystem { parameter });
10551            }
10552            Token::Ident(s) | Token::QuotedIdent(s)
10553                if matches!(
10554                    s.to_ascii_lowercase().as_str(),
10555                    "role" | "user" | "database"
10556                ) && self.peeks_db_role_setting() =>
10557            {
10558                let is_database = s.eq_ignore_ascii_case("database");
10559                return self.parse_db_role_setting(is_database);
10560            }
10561            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10562            // (the non-SET forms; SET/RESET took the branch above). The
10563            // attributes still no-op — recorded, and the ignored PASSWORD
10564            // is ledgered as its own follow-up — but the ROLE is validated:
10565            // any name was accepted for a role that does not exist.
10566            Token::Ident(s) | Token::QuotedIdent(s)
10567                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10568            {
10569                // NB: the enclosing `match self.advance()` already consumed
10570                // ROLE/USER — the round-695 trap, hit again in this round's
10571                // first draft (the name was eaten and WITH parsed as the
10572                // role). The cursor is at the name.
10573                let name = self.expect_ident_or_string()?;
10574                // v7.39 (round 750) — scan the attribute tail for
10575                // PASSWORD. Everything else stays a recorded no-op, but
10576                // a dropped credential rotation is a SECURITY bug:
10577                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10578                // changed nothing, so the old password kept working.
10579                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10580                // NULL` clears the credential.
10581                let mut password: Option<Option<String>> = None;
10582                loop {
10583                    match self.peek() {
10584                        Token::Semicolon | Token::Eof => break,
10585                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10586                            self.advance();
10587                            match self.advance() {
10588                                Token::String(p) => password = Some(Some(p)),
10589                                Token::Null => password = Some(None),
10590                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10591                                    password = Some(None);
10592                                }
10593                                other => {
10594                                    return Err(self.err(alloc::format!(
10595                                        "expected password string or NULL after PASSWORD, got {other:?}"
10596                                    )));
10597                                }
10598                            }
10599                        }
10600                        _ => {
10601                            self.advance();
10602                        }
10603                    }
10604                }
10605                if name.eq_ignore_ascii_case("all") {
10606                    // `ALTER ROLE ALL …` names every role; nothing to check.
10607                    return Ok(Statement::Empty);
10608                }
10609                if let Some(pw) = password {
10610                    return Ok(Statement::AlterRolePassword { name, password: pw });
10611                }
10612                return Ok(Statement::ValidateOnly {
10613                    kind: crate::ast::ValidateOnlyKind::RoleName,
10614                    names: alloc::vec![name],
10615                });
10616            }
10617            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10618            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10619            // list far enough to validate the NAME; the actions still no-op.
10620            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10621            // models none of them and their dumps are rare.)
10622            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10623                let name = self.expect_ident_or_string()?;
10624                self.consume_until_statement_boundary();
10625                return Ok(Statement::ValidateOnly {
10626                    kind: crate::ast::ValidateOnlyKind::CollationName,
10627                    names: alloc::vec![name],
10628                });
10629            }
10630            Token::Ident(s) | Token::QuotedIdent(s)
10631                if s.eq_ignore_ascii_case("text")
10632                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10633                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10634            {
10635                self.advance(); // SEARCH
10636                self.advance(); // CONFIGURATION
10637                let name = self.expect_ident_like()?;
10638                self.consume_until_statement_boundary();
10639                return Ok(Statement::ValidateOnly {
10640                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10641                    names: alloc::vec![name],
10642                });
10643            }
10644            Token::Ident(s) | Token::QuotedIdent(s)
10645                if s.eq_ignore_ascii_case("event")
10646                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10647            {
10648                self.advance(); // TRIGGER
10649                let name = self.expect_ident_like()?;
10650                self.consume_until_statement_boundary();
10651                return Ok(Statement::ValidateOnly {
10652                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10653                    names: alloc::vec![name],
10654                });
10655            }
10656            Token::Ident(s) | Token::QuotedIdent(s)
10657                if s.eq_ignore_ascii_case("large")
10658                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10659            {
10660                self.advance(); // OBJECT
10661                let oid = match self.advance() {
10662                    Token::Integer(n) => alloc::format!("{n}"),
10663                    other => {
10664                        return Err(
10665                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10666                        );
10667                    }
10668                };
10669                self.consume_until_statement_boundary();
10670                return Ok(Statement::ValidateOnly {
10671                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10672                    names: alloc::vec![oid],
10673                });
10674            }
10675            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10676            // argument-list parse as DROP AGGREGATE (round 707); the
10677            // action no-ops, the existence check is real.
10678            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10679                // Same round-695 trap as above: AGGREGATE is already
10680                // consumed; the cursor is at the name.
10681                let name = self.expect_ident_like()?;
10682                let mut names = alloc::vec![name];
10683                if matches!(self.peek(), Token::LParen) {
10684                    self.advance();
10685                    loop {
10686                        match self.peek().clone() {
10687                            Token::RParen => {
10688                                self.advance();
10689                                break;
10690                            }
10691                            Token::Star => {
10692                                self.advance();
10693                                names.push(String::from("*"));
10694                            }
10695                            Token::Comma => {
10696                                self.advance();
10697                            }
10698                            _ => {
10699                                let mut t = self.expect_ident_like()?;
10700                                while let Token::Ident(nx) = self.peek() {
10701                                    let nx = nx.clone();
10702                                    self.advance();
10703                                    t.push(' ');
10704                                    t.push_str(&nx);
10705                                }
10706                                names.push(t);
10707                            }
10708                        }
10709                    }
10710                }
10711                self.consume_until_statement_boundary();
10712                return Ok(Statement::ValidateOnly {
10713                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10714                    names,
10715                });
10716            }
10717            Token::Ident(s) | Token::QuotedIdent(s)
10718                if matches!(
10719                    s.to_ascii_lowercase().as_str(),
10720                    "view"
10721                        | "function"
10722                        | "database"
10723                        | "schema"
10724                        | "owner"
10725                        | "default"
10726                        | "extension"
10727                        | "materialized"
10728                        | "publication"
10729                        | "subscription"
10730                        // v7.37.17 (17.6 siblings) — additional ALTER
10731                        // targets pg_dump / pg_dumpall / operator DB
10732                        // migration scripts commonly emit. SPG has
10733                        // no matching machinery for any of these; the
10734                        // parser accepts + Empty-returns so pg_dump
10735                        // tail statements don't stall.
10736                        | "tablespace"
10737                        | "language"
10738                        | "operator"
10739                        | "conversion"
10740                        | "statistics"
10741                        | "server"
10742                        | "foreign"
10743                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10744                        // / TEMPLATE (CONFIGURATION intercepted above).
10745                        | "text"
10746                ) =>
10747            {
10748                self.consume_until_statement_boundary();
10749                return Ok(Statement::Empty);
10750            }
10751            other => {
10752                return Err(self.err(format!(
10753                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10754                     after ALTER, got {other:?}"
10755                )));
10756            }
10757        }
10758        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10759        // (mailrs migrate-042 ships these). The presence of an
10760        // IF EXISTS makes the subsequent name lookup tolerate
10761        // a missing index — engine returns CommandOk no-op.
10762        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10763            let next = self.tokens.get(self.pos + 1);
10764            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10765                self.advance();
10766                self.advance();
10767                true
10768            } else {
10769                false
10770            }
10771        } else {
10772            false
10773        };
10774        let name = self.expect_ident_like()?;
10775        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10776        // Detect BEFORE the REBUILD path so the existing REBUILD
10777        // arm stays untouched.
10778        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10779            self.advance();
10780            if matches!(self.peek(), Token::To) {
10781                self.advance();
10782            } else {
10783                self.expect_keyword_ident("to")?;
10784            }
10785            let new = self.expect_ident_like()?;
10786            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10787                name,
10788                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10789            }));
10790        }
10791        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10792        // A syntax error before; the index is validated, the params no-op.
10793        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10794            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10795                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10796        {
10797            self.consume_until_statement_boundary();
10798            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10799                name,
10800                target: crate::ast::AlterIndexTarget::StorageParams,
10801            }));
10802        }
10803        // REBUILD
10804        self.expect_keyword_ident("rebuild")?;
10805        // Optional: WITH (encoding = <enc>)
10806        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10807            self.advance();
10808            if !matches!(self.peek(), Token::LParen) {
10809                return Err(self.err(format!(
10810                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10811                    self.peek()
10812                )));
10813            }
10814            self.advance();
10815            self.expect_keyword_ident("encoding")?;
10816            if !matches!(self.peek(), Token::Eq) {
10817                return Err(self.err(format!(
10818                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10819                    self.peek()
10820                )));
10821            }
10822            self.advance();
10823            let enc_ident = match self.advance() {
10824                Token::Ident(s) | Token::QuotedIdent(s) => s,
10825                other => {
10826                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10827                }
10828            };
10829            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10830                "f32" => VecEncoding::F32,
10831                "sq8" => VecEncoding::Sq8,
10832                "half" => VecEncoding::F16,
10833                other => {
10834                    return Err(self.err(format!(
10835                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10836                    )));
10837                }
10838            };
10839            if !matches!(self.peek(), Token::RParen) {
10840                return Err(self.err(format!(
10841                    "expected ')' after encoding value, got {:?}",
10842                    self.peek()
10843                )));
10844            }
10845            self.advance();
10846            Some(enc)
10847        } else {
10848            None
10849        };
10850        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10851            name,
10852            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10853        }))
10854    }
10855
10856    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10857    /// only `SET` form currently supported; future v6.7.x can add
10858    /// more SET subjects without changing the dispatch shape.
10859    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10860    /// subactions. Single-subaction shape stays a 1-element vec.
10861    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10862        let table_name = self.expect_ident_like()?;
10863        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10864        loop {
10865            let subaction = self.parse_alter_table_subaction()?;
10866            // ADD COLUMN with inline REFERENCES emits both an
10867            // AddColumn and an AddForeignKey subaction; the
10868            // helper returns 1 or 2 items.
10869            targets.extend(subaction);
10870            if matches!(self.peek(), Token::Comma) {
10871                self.advance();
10872                continue;
10873            }
10874            break;
10875        }
10876        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10877            name: table_name,
10878            targets,
10879        }))
10880    }
10881
10882    /// Parse one ALTER TABLE subaction. Returns a Vec because
10883    /// inline `REFERENCES` on `ADD COLUMN` produces both an
10884    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10885    fn parse_alter_table_subaction(
10886        &mut self,
10887    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10888        match self.peek() {
10889            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10890                self.advance();
10891                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10892                // storage parameters: paren-prefixed; consume.
10893                if matches!(self.peek(), Token::LParen) {
10894                    self.consume_until_statement_boundary();
10895                    return Ok(Vec::new());
10896                }
10897                let setting = self.expect_ident_like()?;
10898                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10899                    if !matches!(self.peek(), Token::Eq) {
10900                        return Err(self.err(alloc::format!(
10901                            "expected '=' after hot_tier_bytes, got {:?}",
10902                            self.peek()
10903                        )));
10904                    }
10905                    self.advance();
10906                    let n = self.expect_u64_literal()?;
10907                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10908                }
10909                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10910                // accept-and-no-op for ALTER TABLE SET <subject>
10911                // forms that pg_dump emits but SPG either treats
10912                // as N/A (single-tenant, single-owner, no shared
10913                // tablespaces) or accepts the dump-side declaration
10914                // without runtime effect:
10915                //   SET SCHEMA <name>            (18.11)
10916                //   SET TABLESPACE <name>        (18.8)
10917                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
10918                //   SET WITHOUT CLUSTER          (18.13)
10919                //   SET WITHOUT OIDS             (PG legacy)
10920                //   SET (option = value, …)      (storage parameters)
10921                //   SET REPLICA IDENTITY {…}     (18.14)
10922                if setting.eq_ignore_ascii_case("schema")
10923                    || setting.eq_ignore_ascii_case("tablespace")
10924                    || setting.eq_ignore_ascii_case("logged")
10925                    || setting.eq_ignore_ascii_case("unlogged")
10926                    || setting.eq_ignore_ascii_case("without")
10927                {
10928                    self.consume_until_statement_boundary();
10929                    return Ok(Vec::new());
10930                }
10931                if setting.eq_ignore_ascii_case("replica") {
10932                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10933                    self.consume_until_statement_boundary();
10934                    return Ok(Vec::new());
10935                }
10936                // SET (option=value, …) — storage parameters.
10937                if matches!(self.peek(), Token::LParen) {
10938                    self.consume_until_statement_boundary();
10939                    return Ok(Vec::new());
10940                }
10941                Err(self.err(alloc::format!(
10942                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10943                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10944                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10945                )))
10946            }
10947            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10948            // not ignored: round 645 gave SPG the inheritance the
10949            // v7.37.18 no-op said it did not have.
10950            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10951                self.advance();
10952                let parent = self.expect_ident_like()?;
10953                self.consume_until_statement_boundary();
10954                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10955                    parent,
10956                    detach: false
10957                }])
10958            }
10959            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10960            // LEVEL SECURITY`, which has its own RLS arm below — without
10961            // the guard this swallowed NO FORCE as a no-op.
10962            Token::Ident(s)
10963                if s.eq_ignore_ascii_case("no")
10964                    && !matches!(
10965                        self.tokens.get(self.pos + 1),
10966                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10967                    ) =>
10968            {
10969                self.advance();
10970                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10971                    if k.eq_ignore_ascii_case("inherit"))
10972                {
10973                    self.advance();
10974                    let parent = self.expect_ident_like()?;
10975                    self.consume_until_statement_boundary();
10976                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10977                        parent,
10978                        detach: true
10979                    }]);
10980                }
10981                self.consume_until_statement_boundary();
10982                Ok(Vec::new())
10983            }
10984            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10985            // single-owner, so there is still nothing to record.
10986            //
10987            // v7.39 (round 652) — but the name now reaches the engine,
10988            // which refuses a role that does not exist as PG does. The
10989            // no-op was swallowing the whole statement, so a dump naming
10990            // a role this server never heard of restored clean and left
10991            // the table owned by whoever ran the restore.
10992            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10993                self.advance();
10994                if matches!(self.peek(), Token::To) {
10995                    self.advance();
10996                }
10997                let role = self.expect_ident_like()?;
10998                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10999                    role
11000                }])
11001            }
11002            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
11003            // PG sets a hint; SPG doesn't have clustered storage, so the
11004            // hint itself stays a no-op.
11005            //
11006            // v7.39 (round 652) — the index name is checked now. PG
11007            // errors on one that does not exist, and swallowing that let
11008            // a typo'd CLUSTER ON pass silently.
11009            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
11010                self.advance();
11011                // `ON` is a reserved token, not an ident.
11012                if !matches!(self.peek(), Token::On) {
11013                    return Err(self.err(alloc::format!(
11014                        "expected ON after CLUSTER, got {:?}",
11015                        self.peek()
11016                    )));
11017                }
11018                self.advance();
11019                let index = self.expect_ident_like()?;
11020                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
11021                    index: Some(index)
11022                }])
11023            }
11024            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
11025            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
11026            // what a logical decoder puts in the old-tuple image; SPG's
11027            // replication is SQL-text, so there is nothing to record.
11028            // Accept-and-no-op (it used to be a parse error).
11029            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
11030                self.advance();
11031                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
11032                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
11033                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
11034                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
11035                {
11036                    self.advance(); // IDENTITY
11037                    self.advance(); // USING
11038                    if matches!(self.peek(), Token::Index)
11039                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
11040                    {
11041                        self.advance();
11042                    }
11043                    let index = self.expect_ident_like()?;
11044                    self.consume_until_statement_boundary();
11045                    return Ok(alloc::vec![
11046                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
11047                    ]);
11048                }
11049                self.consume_until_statement_boundary();
11050                Ok(Vec::new())
11051            }
11052            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
11053            //
11054            // v7.39 (round 652) — it used to consume the statement and
11055            // return nothing, on the stated theory that SPG validated at
11056            // ADD CONSTRAINT time so there was never anything left to
11057            // validate. Measured against PG18, ADD CONSTRAINT did not
11058            // scan the existing rows at all — the comment described a
11059            // property SPG did not have, which is why nobody looked. Both
11060            // halves are real now: ADD scans unless told NOT VALID, and
11061            // this scans what NOT VALID skipped.
11062            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
11063                self.advance();
11064                self.expect_keyword_ident("constraint")?;
11065                let name = self.expect_ident_like()?;
11066                Ok(alloc::vec![
11067                    crate::ast::AlterTableTarget::ValidateConstraint { name }
11068                ])
11069            }
11070            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
11071            // SET (option = value, …). PG uses it to clear per-table
11072            // storage params like fillfactor or autovacuum_*. SPG
11073            // engine-manages those parameters; accept-and-no-op.
11074            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
11075                self.advance();
11076                self.consume_until_statement_boundary();
11077                Ok(Vec::new())
11078            }
11079            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
11080            // type-of binding (PG 9.0+). SPG composite types
11081            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
11082            // TABLE OF is rare and inverse of CREATE TABLE OF.
11083            // Accept-and-no-op until a customer dump round-trips it.
11084            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
11085                self.advance();
11086                // v7.39 (round 710) — the type name is validated now.
11087                let type_name = self.expect_ident_like()?;
11088                self.consume_until_statement_boundary();
11089                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
11090                    type_name
11091                }])
11092            }
11093            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
11094            // (reserved keyword) rather than Token::Ident("not"),
11095            // so it needs its own arm. Accept-and-no-op same as OF.
11096            Token::Not => {
11097                self.advance();
11098                self.consume_until_statement_boundary();
11099                Ok(Vec::new())
11100            }
11101            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
11102            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
11103                self.advance();
11104                self.expect_row_level_security()?;
11105                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11106                    enabled: None,
11107                    force: Some(true),
11108                }])
11109            }
11110            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
11111            Token::Ident(s)
11112                if s.eq_ignore_ascii_case("no")
11113                    && matches!(
11114                        self.tokens.get(self.pos + 1),
11115                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11116                    ) =>
11117            {
11118                self.advance(); // NO
11119                self.advance(); // FORCE
11120                self.expect_row_level_security()?;
11121                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11122                    enabled: None,
11123                    force: Some(false),
11124                }])
11125            }
11126            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
11127            // (sets relrowsecurity). The guard requires the next token to be
11128            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
11129            Token::Ident(s)
11130                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
11131                    && matches!(
11132                        self.tokens.get(self.pos + 1),
11133                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
11134                    ) =>
11135            {
11136                let enabled = s.eq_ignore_ascii_case("enable");
11137                self.advance(); // ENABLE/DISABLE
11138                self.expect_row_level_security()?;
11139                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11140                    enabled: Some(enabled),
11141                    force: None,
11142                }])
11143            }
11144            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11145                self.advance();
11146                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
11147                // {INDEX|KEY} [name] (cols)`, which every ORM migration
11148                // emits. The same grammar CREATE TABLE already accepts
11149                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11150                // through the SAME parser — an ALTER-only copy would be a
11151                // second place for the two to drift.
11152                if self.peek_mysql_inline_key_start() {
11153                    return Ok(match self.parse_mysql_inline_key()? {
11154                        Some(c) => {
11155                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11156                        }
11157                        // FULLTEXT / SPATIAL parse and are accepted as a
11158                        // no-op here exactly as they are inline.
11159                        None => Vec::new(),
11160                    });
11161                }
11162                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11163                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11164                // PRIMARY KEY this way; mysqldump emits both.
11165                // Peek-only dispatch (no advance) — `advance()`
11166                // destructively replaces consumed tokens with Eof,
11167                // so saved-pos restore would land on Eofs.
11168                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11169                {
11170                    // The next-but-one ident is the constraint
11171                    // name; the one after THAT is the kind.
11172                    let kind_pos = self.pos + 2;
11173                    let kind = self.tokens.get(kind_pos).cloned();
11174                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11175                    {
11176                        let fk = self.parse_table_level_fk()?;
11177                        return Ok(alloc::vec![
11178                            crate::ast::AlterTableTarget::AddForeignKey(fk)
11179                        ]);
11180                    }
11181                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11182                    {
11183                        self.advance(); // CONSTRAINT
11184                        // v7.39 (read01 round 48) — keep the name; the engine
11185                        // stores it now instead of dropping it on the floor.
11186                        let con_name = self.expect_ident_like()?;
11187                        self.advance(); // PRIMARY
11188                        self.expect_keyword_ident("key")?;
11189                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11190                        // v7.39 (round 711) — the ALTER form carries the
11191                        // timing too (pg_dump writes it here).
11192                        let (deferrable, initially_deferred) =
11193                            self.consume_deferrable_clauses_timed()?;
11194                        return Ok(alloc::vec![
11195                            crate::ast::AlterTableTarget::AddTableConstraint(
11196                                crate::ast::TableConstraint::PrimaryKey {
11197                                    name: Some(con_name),
11198                                    columns: cols,
11199                                    deferrable,
11200                                    initially_deferred,
11201                                }
11202                            )
11203                        ]);
11204                    }
11205                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11206                    {
11207                        self.advance(); // CONSTRAINT
11208                        // v7.39 (read01 round 48) — keep the name.
11209                        let con_name = self.expect_ident_like()?;
11210                        // v7.22 (mailrs round-13 gap 6) — delegate so
11211                        // the optional `NULLS [NOT] DISTINCT` modifier
11212                        // parses here too (pg_dump emits the ALTER
11213                        // form; semantics enforced by the engine
11214                        // since v7.13).
11215                        let mut uc = self.parse_table_level_unique()?;
11216                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11217                            *name = Some(con_name);
11218                        }
11219                        return Ok(alloc::vec![
11220                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11221                        ]);
11222                    }
11223                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11224                    {
11225                        self.advance(); // CONSTRAINT
11226                        // v7.39 (read01 round 48) — keep the name.
11227                        let con_name = self.expect_ident_like()?;
11228                        self.advance(); // CHECK
11229                        if !matches!(self.peek(), Token::LParen) {
11230                            return Err(self.err(alloc::format!(
11231                                "expected '(' after CHECK, got {:?}", self.peek()
11232                            )));
11233                        }
11234                        self.advance();
11235                        let expr = self.parse_expr(0)?;
11236                        if matches!(self.peek(), Token::RParen) {
11237                            self.advance();
11238                        }
11239                        let not_valid = self.parse_not_valid_suffix();
11240                        return Ok(alloc::vec![
11241                            crate::ast::AlterTableTarget::AddTableConstraint(
11242                                crate::ast::TableConstraint::Check {
11243                                    name: Some(con_name),
11244                                    expr,
11245                                    not_valid,
11246                                }
11247                            )
11248                        ]);
11249                    }
11250                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11251                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11252                    // exclusion constraints via this ALTER form.
11253                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11254                    {
11255                        self.advance(); // CONSTRAINT
11256                        let con_name = self.expect_ident_like()?;
11257                        let mut ex = self.parse_table_level_exclude()?;
11258                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11259                            *name = Some(con_name);
11260                        }
11261                        return Ok(alloc::vec![
11262                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11263                        ]);
11264                    }
11265                    // Unknown kind — fall through to FK path which
11266                    // produces a descriptive parse error.
11267                }
11268                let is_fk = matches!(
11269                    self.peek(),
11270                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11271                        || s.eq_ignore_ascii_case("foreign")
11272                );
11273                if is_fk {
11274                    let fk = self.parse_table_level_fk()?;
11275                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11276                }
11277                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11278                // (no CONSTRAINT prefix) — same dispatch.
11279                match self.peek().clone() {
11280                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11281                        self.advance();
11282                        self.expect_keyword_ident("key")?;
11283                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11284                        let (deferrable, initially_deferred) =
11285                            self.consume_deferrable_clauses_timed()?;
11286                        return Ok(alloc::vec![
11287                            crate::ast::AlterTableTarget::AddTableConstraint(
11288                                crate::ast::TableConstraint::PrimaryKey {
11289                                    name: None,
11290                                    columns: cols,
11291                                    deferrable,
11292                                    initially_deferred,
11293                                }
11294                            )
11295                        ]);
11296                    }
11297                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11298                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
11299                        let uc = self.parse_table_level_unique()?;
11300                        return Ok(alloc::vec![
11301                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11302                        ]);
11303                    }
11304                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11305                    // prefix). The other three bare forms were here and
11306                    // this one was not, so it fell through to the column
11307                    // path and came back as "unexpected reserved keyword
11308                    // 'check' at start of column definition".
11309                    _ if self.peek_table_level_check_start() => {
11310                        let chk = self.parse_table_level_check()?;
11311                        let not_valid = self.parse_not_valid_suffix();
11312                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11313                            unreachable!("parse_table_level_check returns Check")
11314                        };
11315                        return Ok(alloc::vec![
11316                            crate::ast::AlterTableTarget::AddTableConstraint(
11317                                crate::ast::TableConstraint::Check {
11318                                    name: None,
11319                                    expr,
11320                                    not_valid,
11321                                }
11322                            )
11323                        ]);
11324                    }
11325                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11326                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11327                        let ex = self.parse_table_level_exclude()?;
11328                        return Ok(alloc::vec![
11329                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11330                        ]);
11331                    }
11332                    _ => {}
11333                }
11334                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11335                    self.advance();
11336                }
11337                let mut if_not_exists = false;
11338                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11339                    self.advance();
11340                    if !matches!(self.peek(), Token::Not) {
11341                        return Err(self.err(alloc::format!(
11342                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11343                            self.peek()
11344                        )));
11345                    }
11346                    self.advance();
11347                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11348                        return Err(self.err(alloc::format!(
11349                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11350                            self.peek()
11351                        )));
11352                    }
11353                    self.advance();
11354                    if_not_exists = true;
11355                }
11356                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11357                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11358                // returns ColumnDef + an optional inline FK.
11359                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11360                let col_name = column.name.clone();
11361                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11362                    column,
11363                    if_not_exists,
11364                }];
11365                if let Some(mut fk) = col_level_fk {
11366                    if fk.columns.is_empty() {
11367                        fk.columns.push(col_name);
11368                    }
11369                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11370                }
11371                Ok(out)
11372            }
11373            Token::Drop => {
11374                self.advance();
11375                // v7.13.3 — dispatch on the next token. mailrs round-7
11376                // S8 closed DROP COLUMN; round-6 S7 closed
11377                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11378                // RESTRICT modifiers.
11379                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11380                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11381                let subject = match self.peek() {
11382                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11383                        self.advance();
11384                        "constraint"
11385                    }
11386                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11387                        self.advance();
11388                        "column"
11389                    }
11390                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11391                    // `INDEX` lexes as the reserved Token::Index, so it is
11392                    // unambiguous. `KEY` is a plain ident, and PG allows a
11393                    // column literally named "key", so only read it as the
11394                    // keyword when a name follows it.
11395                    Token::Index => {
11396                        self.advance();
11397                        "index"
11398                    }
11399                    Token::Ident(s)
11400                        if s.eq_ignore_ascii_case("key")
11401                            && matches!(
11402                                self.tokens.get(self.pos + 1),
11403                                Some(Token::Ident(_) | Token::QuotedIdent(_))
11404                            ) =>
11405                    {
11406                        self.advance();
11407                        "index"
11408                    }
11409                    // PG-canonical bare `DROP <col>` without COLUMN
11410                    // keyword is also valid; treat any other ident
11411                    // as the column name.
11412                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11413                    other => {
11414                        return Err(self.err(alloc::format!(
11415                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11416                        )));
11417                    }
11418                };
11419                let mut if_exists = false;
11420                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11421                    let n1 = self.tokens.get(self.pos + 1);
11422                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11423                        self.advance();
11424                        self.advance();
11425                        if_exists = true;
11426                    }
11427                }
11428                let name = self.expect_ident_like()?;
11429                let mut cascade = false;
11430                if matches!(
11431                    self.peek(),
11432                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11433                        || s.eq_ignore_ascii_case("restrict")
11434                ) {
11435                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11436                    {
11437                        cascade = true;
11438                    }
11439                    self.advance();
11440                }
11441                if subject == "index" {
11442                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11443                        name,
11444                        if_exists,
11445                    }])
11446                } else if subject == "constraint" {
11447                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11448                        name,
11449                        if_exists,
11450                    }])
11451                } else {
11452                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11453                        column: name,
11454                        if_exists,
11455                        cascade,
11456                    }])
11457                }
11458            }
11459            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11460                self.advance();
11461                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11462                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11463                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11464                // immediately; accept-and-no-op.
11465                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11466                    self.advance();
11467                    self.consume_until_statement_boundary();
11468                    return Ok(Vec::new());
11469                }
11470                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11471                    self.advance();
11472                }
11473                let col_name = self.expect_ident_like()?;
11474                match self.peek() {
11475                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11476                        self.advance();
11477                    }
11478                    // v7.14.0 — pg_dump emits BIGSERIAL via
11479                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11480                    // nextval('seq')` (the sequence is created
11481                    // separately). SPG's BIGSERIAL already uses
11482                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11483                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11484                    // engine no-ops by consuming the tail.
11485                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11486                        // v7.22 (round-13 T2) — `SET DEFAULT
11487                        // nextval('…')` is how pg_dump spells a
11488                        // SERIAL column (plain integer in CREATE
11489                        // TABLE + this ALTER). It used to be
11490                        // swallowed as a no-op, which silently
11491                        // STRIPPED auto-increment from imported
11492                        // schemas — the first post-import INSERT
11493                        // without an explicit id then violated NOT
11494                        // NULL. Lower it to the auto-increment
11495                        // marker instead.
11496                        let is_default_nextval =
11497                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11498                                && matches!(
11499                                    self.tokens.get(self.pos + 2),
11500                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11501                                );
11502                        if is_default_nextval {
11503                            let seq_name = self.scan_sequence_name_until_boundary();
11504                            return Ok(alloc::vec![
11505                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11506                                    column: col_name,
11507                                    seq_name,
11508                                }
11509                            ]);
11510                        }
11511                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11512                        self.advance(); // consume "set"
11513                        match self.peek().clone() {
11514                            Token::Default => {
11515                                self.advance();
11516                                let default_expr = self.parse_expr(0)?;
11517                                return Ok(alloc::vec![
11518                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11519                                        column: col_name,
11520                                        default_expr,
11521                                    }
11522                                ]);
11523                            }
11524                            Token::Not => {
11525                                self.advance();
11526                                if !matches!(self.peek(), Token::Null) {
11527                                    return Err(self.err(alloc::format!(
11528                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11529                                        self.peek()
11530                                    )));
11531                                }
11532                                self.advance();
11533                                return Ok(alloc::vec![
11534                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11535                                        column: col_name,
11536                                    }
11537                                ]);
11538                            }
11539                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11540                            // stored generated column's expression and
11541                            // recompute existing rows.
11542                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11543                                self.advance(); // EXPRESSION
11544                                if matches!(self.peek(), Token::As) {
11545                                    self.advance();
11546                                }
11547                                let expr = self.parse_expr(0)?;
11548                                return Ok(alloc::vec![
11549                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11550                                        column: col_name,
11551                                        expr,
11552                                    }
11553                                ]);
11554                            }
11555                            other => {
11556                                // Other SET subjects (STATISTICS,
11557                                // STORAGE, COMPRESSION, …) stay no-ops —
11558                                // storage hints with no SPG semantics.
11559                                let _ = other;
11560                                self.consume_until_statement_boundary();
11561                                return Ok(Vec::new());
11562                            }
11563                        }
11564                    }
11565                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11566                        self.advance(); // consume "drop"
11567                        return self.parse_alter_column_drop_tail(col_name);
11568                    }
11569                    Token::Drop => {
11570                        self.advance(); // consume Drop token
11571                        return self.parse_alter_column_drop_tail(col_name);
11572                    }
11573                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11574                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11575                        // GENERATED { ALWAYS | BY DEFAULT } AS
11576                        // IDENTITY ( … )`: pg_dump's spelling for
11577                        // identity columns. Same auto-increment
11578                        // lowering as the nextval default; the
11579                        // sequence options inside the parens are
11580                        // no-ops under SPG's max+1 semantics.
11581                        let is_generated = matches!(
11582                            self.tokens.get(self.pos + 1),
11583                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11584                        );
11585                        if !is_generated {
11586                            return Err(self.err(alloc::format!(
11587                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11588                                self.tokens.get(self.pos + 1)
11589                            )));
11590                        }
11591                        let seq_name = self.scan_sequence_name_until_boundary();
11592                        return Ok(alloc::vec![
11593                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11594                                column: col_name,
11595                                seq_name,
11596                            }
11597                        ]);
11598                    }
11599                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11600                    // column: floor the next allocated value at n (bare
11601                    // RESTART = restart from the start value, 1).
11602                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11603                        self.advance();
11604                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11605                        {
11606                            self.advance();
11607                            let neg = if matches!(self.peek(), Token::Minus) {
11608                                self.advance();
11609                                true
11610                            } else {
11611                                false
11612                            };
11613                            match self.advance() {
11614                                Token::Integer(v) => Some(if neg { -v } else { v }),
11615                                other => {
11616                                    return Err(self.err(alloc::format!(
11617                                        "expected integer after RESTART WITH, got {other:?}"
11618                                    )));
11619                                }
11620                            }
11621                        } else {
11622                            None
11623                        };
11624                        return Ok(alloc::vec![
11625                            crate::ast::AlterTableTarget::AlterColumnRestart {
11626                                column: col_name,
11627                                with,
11628                            }
11629                        ]);
11630                    }
11631                    other => {
11632                        return Err(self.err(alloc::format!(
11633                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11634                        )));
11635                    }
11636                }
11637                // v7.39 (round 713) — the type parser has consumed a
11638                // trailing `COLLATE <name>` since Phase 2.5, and
11639                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11640                // TYPE text COLLATE "C"` parsed clean and changed
11641                // nothing. Keep the clause; the engine re-collates.
11642                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _, _, _) =
11643                    self.parse_type_with_implied_flags()?;
11644                let collation = if coll_explicit {
11645                    coll_name.map(|n| (coll, n))
11646                } else {
11647                    None
11648                };
11649                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11650                {
11651                    self.advance();
11652                    Some(self.parse_expr(0)?)
11653                } else {
11654                    None
11655                };
11656                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11657                    column: col_name,
11658                    new_type,
11659                    using,
11660                    collation,
11661                }])
11662            }
11663            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11664            // PG also supports `RENAME TO new_table` for table-name
11665            // rename; that surface is deferred (pg_dump never emits
11666            // it). If the first post-RENAME ident is `TO`, the user
11667            // is asking for table rename — error with a clear
11668            // message rather than misparsing `TO` as a column name.
11669            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11670                self.advance();
11671                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11672                // table-name rename (mailrs round-10 A.5 — used
11673                // by migrate-042's `RENAME TO email_contacts`).
11674                // `TO` lexes as Token::To.
11675                if matches!(self.peek(), Token::To)
11676                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11677                {
11678                    self.advance();
11679                    let new = self.expect_ident_like()?;
11680                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11681                        new,
11682                    }]);
11683                }
11684                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11685                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11686                    self.advance();
11687                    let old = self.expect_ident_like()?;
11688                    if matches!(self.peek(), Token::To) {
11689                        self.advance();
11690                    } else {
11691                        self.expect_keyword_ident("to")?;
11692                    }
11693                    let new = self.expect_ident_like()?;
11694                    return Ok(alloc::vec![
11695                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11696                    ]);
11697                }
11698                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11699                    self.advance();
11700                }
11701                let old = self.expect_ident_like()?;
11702                // `TO` is a reserved keyword token; accept both
11703                // Token::To and Token::Ident("to") for consistency.
11704                if matches!(self.peek(), Token::To) {
11705                    self.advance();
11706                } else {
11707                    self.expect_keyword_ident("to")?;
11708                }
11709                let new = self.expect_ident_like()?;
11710                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11711                    old,
11712                    new,
11713                }])
11714            }
11715            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11716            // { ALL | <name> }`. pg_dump --disable-triggers wraps
11717            // every data block with these. Real disable semantics —
11718            // not no-op — because reload correctness assumes the
11719            // triggers don't fire (rows already carry their
11720            // computed values from prod).
11721            Token::Ident(s)
11722                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11723            {
11724                let enabled = s.eq_ignore_ascii_case("enable");
11725                self.advance();
11726                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11727                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11728                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11729                // pg_dump output) — anything else falls through to
11730                // the catch-all error below.
11731                // v7.22 (round-13 T3) — mysqldump wraps every data
11732                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11733                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11734                // maintains indexes incrementally — engine no-op.
11735                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11736                    self.advance();
11737                    return Ok(Vec::new());
11738                }
11739                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11740                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11741                // to gate triggers on session_replication_role; SPG
11742                // has no replica role, so the prefix is consumed and
11743                // treated identically to the plain ENABLE/DISABLE
11744                // TRIGGER form.
11745                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11746                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11747                {
11748                    self.advance();
11749                }
11750                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11751                    return Err(self.err(alloc::format!(
11752                        "expected TRIGGER after {}, got {:?}",
11753                        if enabled { "ENABLE" } else { "DISABLE" },
11754                        self.peek()
11755                    )));
11756                }
11757                self.advance();
11758                // `ALL` lexes as Token::All (reserved); also
11759                // accept Token::Ident("all") for symmetry.
11760                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11761                // TRIGGER selectors. USER (= all user triggers) is
11762                // semantically ALL here; REPLICA / ALWAYS gate on
11763                // session_replication_role which SPG doesn't track.
11764                // All map to TriggerSelector::All.
11765                let which = if matches!(self.peek(), Token::All)
11766                    || matches!(self.peek(), Token::Ident(s)
11767                        if s.eq_ignore_ascii_case("all")
11768                            || s.eq_ignore_ascii_case("user")
11769                            || s.eq_ignore_ascii_case("replica")
11770                            || s.eq_ignore_ascii_case("always"))
11771                {
11772                    self.advance();
11773                    crate::ast::TriggerSelector::All
11774                } else {
11775                    let name = self.expect_ident_like()?;
11776                    crate::ast::TriggerSelector::Named(name)
11777                };
11778                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11779                    which,
11780                    enabled,
11781                }])
11782            }
11783            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11784            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11785                self.advance();
11786                if !matches!(self.peek(), Token::Partition)
11787                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11788                        if s.eq_ignore_ascii_case("partition"))
11789                {
11790                    return Err(self.err(alloc::format!(
11791                        "expected PARTITION after ATTACH, got {:?}",
11792                        self.peek()
11793                    )));
11794                }
11795                self.advance();
11796                let child = self.expect_ident_like()?;
11797                let bounds = self.parse_partition_bounds_tail()?;
11798                Ok(alloc::vec![
11799                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11800                ])
11801            }
11802            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11803            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11804                self.advance();
11805                if !matches!(self.peek(), Token::Partition)
11806                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11807                        if s.eq_ignore_ascii_case("partition"))
11808                {
11809                    return Err(self.err(alloc::format!(
11810                        "expected PARTITION after DETACH, got {:?}",
11811                        self.peek()
11812                    )));
11813                }
11814                self.advance();
11815                let child = self.expect_ident_like()?;
11816                let mut concurrently = false;
11817                let mut finalize = false;
11818                loop {
11819                    match self.peek().clone() {
11820                        Token::Ident(s) | Token::QuotedIdent(s)
11821                            if s.eq_ignore_ascii_case("concurrently") =>
11822                        {
11823                            self.advance();
11824                            concurrently = true;
11825                        }
11826                        Token::Ident(s) | Token::QuotedIdent(s)
11827                            if s.eq_ignore_ascii_case("finalize") =>
11828                        {
11829                            self.advance();
11830                            finalize = true;
11831                        }
11832                        _ => break,
11833                    }
11834                }
11835                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11836                    child,
11837                    concurrently,
11838                    finalize,
11839                }])
11840            }
11841            other => Err(self.err(alloc::format!(
11842                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11843            ))),
11844        }
11845    }
11846
11847    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11848    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11849    /// TABLE … ATTACH PARTITION. Shares the same grammar as
11850    /// `parse_partition_of_tail`'s bounds branch.
11851    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11852    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11853    /// lowering each to the respective AlterTableTarget. Any
11854    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11855    /// no-op via consume_until_statement_boundary.
11856    fn parse_alter_column_drop_tail(
11857        &mut self,
11858        col_name: String,
11859    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11860        match self.peek().clone() {
11861            Token::Default => {
11862                self.advance();
11863                Ok(alloc::vec![
11864                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11865                ])
11866            }
11867            Token::Not => {
11868                self.advance();
11869                if !matches!(self.peek(), Token::Null) {
11870                    return Err(self.err(alloc::format!(
11871                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11872                        self.peek()
11873                    )));
11874                }
11875                self.advance();
11876                Ok(alloc::vec![
11877                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11878                ])
11879            }
11880            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11881            // generated column into a plain column.
11882            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11883                self.advance();
11884                // v7.39 (round 187, U10) — IF EXISTS was consumed but
11885                // dropped, so the engine still errored on a plain
11886                // column; PG's semantics are NOTICE + skip.
11887                let mut if_exists = false;
11888                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11889                    self.advance();
11890                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11891                        self.advance();
11892                        if_exists = true;
11893                    }
11894                }
11895                Ok(alloc::vec![
11896                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
11897                        column: col_name,
11898                        if_exists,
11899                    }
11900                ])
11901            }
11902            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11903            // identity column into a plain column.
11904            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11905                self.advance();
11906                let mut if_exists = false;
11907                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11908                    self.advance();
11909                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11910                        self.advance();
11911                        if_exists = true;
11912                    }
11913                }
11914                Ok(alloc::vec![
11915                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11916                        column: col_name,
11917                        if_exists,
11918                    }
11919                ])
11920            }
11921            _ => {
11922                self.consume_until_statement_boundary();
11923                Ok(Vec::new())
11924            }
11925        }
11926    }
11927
11928    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11929    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11930    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11931    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11932    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11933        let mut opts = crate::ast::CopyOptions::default();
11934        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11935            return Ok(opts);
11936        }
11937        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11938            self.advance();
11939        }
11940        if matches!(self.peek(), Token::LParen) {
11941            self.advance();
11942            loop {
11943                self.parse_one_copy_option(&mut opts)?;
11944                match self.peek() {
11945                    Token::Comma => {
11946                        self.advance();
11947                    }
11948                    Token::RParen => {
11949                        self.advance();
11950                        break;
11951                    }
11952                    other => {
11953                        return Err(self.err(alloc::format!(
11954                            "expected ',' or ')' in COPY options, got {other:?}"
11955                        )));
11956                    }
11957                }
11958            }
11959        } else {
11960            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11961                self.parse_one_copy_option(&mut opts)?;
11962            }
11963        }
11964        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11965            return Err(self.err(alloc::format!(
11966                "unexpected token after COPY options: {:?}",
11967                self.peek()
11968            )));
11969        }
11970        Ok(opts)
11971    }
11972
11973    fn parse_one_copy_option(
11974        &mut self,
11975        opts: &mut crate::ast::CopyOptions,
11976    ) -> Result<(), ParseError> {
11977        use crate::ast::CopyFormat;
11978        // The option keyword. NULL lexes as its own token; the rest are
11979        // bare identifiers.
11980        let kw = match self.advance() {
11981            Token::Null => alloc::string::String::from("NULL"),
11982            Token::Ident(s) => s.to_uppercase(),
11983            other => {
11984                return Err(self.err(alloc::format!(
11985                    "expected a COPY option keyword, got {other:?}"
11986                )));
11987            }
11988        };
11989        match kw.as_str() {
11990            "FORMAT" => {
11991                let fmt = self.expect_ident_like()?;
11992                match fmt.to_ascii_uppercase().as_str() {
11993                    "CSV" => opts.format = CopyFormat::Csv,
11994                    "TEXT" => opts.format = CopyFormat::Text,
11995                    other => {
11996                        return Err(self.err(alloc::format!(
11997                            "COPY format \"{}\" not recognized",
11998                            other.to_ascii_lowercase()
11999                        )));
12000                    }
12001                }
12002            }
12003            // Legacy bare format keywords.
12004            "CSV" => opts.format = CopyFormat::Csv,
12005            "TEXT" => opts.format = CopyFormat::Text,
12006            "HEADER" => {
12007                opts.header = match self.peek() {
12008                    Token::True => {
12009                        self.advance();
12010                        true
12011                    }
12012                    Token::False => {
12013                        self.advance();
12014                        false
12015                    }
12016                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
12017                        self.advance();
12018                        true
12019                    }
12020                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
12021                        self.advance();
12022                        false
12023                    }
12024                    // Bare HEADER (no boolean) means HEADER true.
12025                    _ => true,
12026                };
12027            }
12028            // r1066 (7.38 S5.1) — pgbench 14+ loads with
12029            // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
12030            // vacuum bookkeeping on a freshly created/truncated
12031            // table; SPG's per-statement visibility makes it a
12032            // faithful no-op, and rejecting it aborted `pgbench -i`
12033            // against the drop-in. Accept ON/OFF/bare, change nothing.
12034            "FREEZE" => match self.peek() {
12035                Token::True | Token::False => {
12036                    self.advance();
12037                }
12038                Token::Ident(s)
12039                    if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
12040                {
12041                    self.advance();
12042                }
12043                _ => {}
12044            },
12045            "DELIMITER" | "QUOTE" | "ESCAPE" => {
12046                let s = match self.advance() {
12047                    Token::String(s) => s,
12048                    other => {
12049                        return Err(self.err(alloc::format!(
12050                            "COPY {kw} expects a single-character string, got {other:?}"
12051                        )));
12052                    }
12053                };
12054                // v7.39 (round 247) — PG's wording (0A000), keyword in
12055                // lowercase: "COPY delimiter must be a single one-byte
12056                // character".
12057                let one_byte_err = || {
12058                    self.err(alloc::format!(
12059                        "COPY {} must be a single one-byte character",
12060                        kw.to_ascii_lowercase()
12061                    ))
12062                };
12063                let mut chars = s.chars();
12064                let c = chars.next().ok_or_else(one_byte_err)?;
12065                if chars.next().is_some() || c.len_utf8() != 1 {
12066                    return Err(one_byte_err());
12067                }
12068                match kw.as_str() {
12069                    "DELIMITER" => opts.delimiter = Some(c),
12070                    "QUOTE" => opts.quote = Some(c),
12071                    _ => opts.escape = Some(c),
12072                }
12073            }
12074            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
12075            "FORCE_QUOTE" => {
12076                if matches!(self.peek(), Token::Star) {
12077                    self.advance();
12078                    opts.force_quote = Some(Vec::new());
12079                } else {
12080                    if !matches!(self.peek(), Token::LParen) {
12081                        return Err(self.err(alloc::format!(
12082                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
12083                            self.peek()
12084                        )));
12085                    }
12086                    self.advance();
12087                    let mut cols = Vec::new();
12088                    loop {
12089                        cols.push(self.expect_ident_like()?);
12090                        match self.peek() {
12091                            Token::Comma => {
12092                                self.advance();
12093                            }
12094                            Token::RParen => {
12095                                self.advance();
12096                                break;
12097                            }
12098                            other => {
12099                                return Err(self.err(alloc::format!(
12100                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
12101                                )));
12102                            }
12103                        }
12104                    }
12105                    opts.force_quote = Some(cols);
12106                }
12107            }
12108            "NULL" => {
12109                opts.null_str = Some(match self.advance() {
12110                    Token::String(s) => s,
12111                    other => {
12112                        return Err(self.err(alloc::format!(
12113                            "COPY NULL expects a quoted string, got {other:?}"
12114                        )));
12115                    }
12116                });
12117            }
12118            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
12119            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
12120            // FORCE_NULL too.
12121            "FORCE_NOT_NULL" | "FORCE_NULL" => {
12122                let cols = self.parse_copy_column_list(&kw)?;
12123                if kw == "FORCE_NOT_NULL" {
12124                    opts.force_not_null = Some(cols);
12125                } else {
12126                    opts.force_null = Some(cols);
12127                }
12128            }
12129            other => {
12130                // PG's wording, lowercased option name.
12131                return Err(self.err(alloc::format!(
12132                    "option \"{}\" not recognized",
12133                    other.to_ascii_lowercase()
12134                )));
12135            }
12136        }
12137        Ok(())
12138    }
12139
12140    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
12141    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
12142    /// is the `*` spelling.
12143    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
12144        if matches!(self.peek(), Token::Star) {
12145            self.advance();
12146            return Ok(Vec::new());
12147        }
12148        if !matches!(self.peek(), Token::LParen) {
12149            return Err(self.err(alloc::format!(
12150                "expected '(' or '*' after {kw}, got {:?}",
12151                self.peek()
12152            )));
12153        }
12154        self.advance();
12155        let mut cols = Vec::new();
12156        loop {
12157            cols.push(self.expect_ident_like()?);
12158            match self.peek() {
12159                Token::Comma => {
12160                    self.advance();
12161                }
12162                Token::RParen => {
12163                    self.advance();
12164                    break;
12165                }
12166                other => {
12167                    return Err(self.err(alloc::format!(
12168                        "expected ',' or ')' in {kw} list, got {other:?}"
12169                    )));
12170                }
12171            }
12172        }
12173        Ok(cols)
12174    }
12175
12176    fn parse_partition_bounds_tail(
12177        &mut self,
12178    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12179        use crate::ast::PartitionOfBoundsAst;
12180        match self.peek() {
12181            Token::Default => {
12182                self.advance();
12183                Ok(PartitionOfBoundsAst::Default)
12184            }
12185            Token::For => {
12186                self.advance();
12187                if !matches!(self.peek(), Token::Values) {
12188                    return Err(
12189                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12190                    );
12191                }
12192                self.advance();
12193                let want_with = matches!(
12194                    self.peek(),
12195                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12196                );
12197                if want_with {
12198                    self.advance();
12199                    if !matches!(self.peek(), Token::LParen) {
12200                        return Err(self.err(format!(
12201                            "expected '(' after FOR VALUES WITH, got {:?}",
12202                            self.peek()
12203                        )));
12204                    }
12205                    self.advance();
12206                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12207                    loop {
12208                        let key = self.expect_ident_like()?;
12209                        let n = match self.peek().clone() {
12210                            Token::Integer(v) if u32::try_from(v).is_ok() => {
12211                                self.advance();
12212                                v as u32
12213                            }
12214                            other => {
12215                                return Err(self.err(format!(
12216                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12217                                )));
12218                            }
12219                        };
12220                        match key.to_ascii_uppercase().as_str() {
12221                            "MODULUS" => modulus = Some(n),
12222                            "REMAINDER" => remainder = Some(n),
12223                            other => {
12224                                return Err(self.err(format!(
12225                                    "FOR VALUES WITH: unknown key {other:?}; \
12226                                     expected MODULUS or REMAINDER"
12227                                )));
12228                            }
12229                        }
12230                        match self.peek() {
12231                            Token::Comma => {
12232                                self.advance();
12233                            }
12234                            Token::RParen => {
12235                                self.advance();
12236                                break;
12237                            }
12238                            other => {
12239                                return Err(self.err(format!(
12240                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12241                                )));
12242                            }
12243                        }
12244                    }
12245                    let modulus = modulus
12246                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12247                    let remainder = remainder.ok_or_else(|| {
12248                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12249                    })?;
12250                    if modulus == 0 {
12251                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12252                    }
12253                    if remainder >= modulus {
12254                        return Err(self.err(format!(
12255                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12256                        )));
12257                    }
12258                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12259                }
12260                match self.peek() {
12261                    Token::From => {
12262                        self.advance();
12263                        let lower = Box::new(self.parse_partition_bound_expr()?);
12264                        if !matches!(self.peek(), Token::To) {
12265                            return Err(self.err(format!(
12266                                "expected TO after FROM (...), got {:?}",
12267                                self.peek()
12268                            )));
12269                        }
12270                        self.advance();
12271                        let upper = Box::new(self.parse_partition_bound_expr()?);
12272                        Ok(PartitionOfBoundsAst::Range { lower, upper })
12273                    }
12274                    Token::In => {
12275                        self.advance();
12276                        if !matches!(self.peek(), Token::LParen) {
12277                            return Err(self.err(format!(
12278                                "expected '(' after FOR VALUES IN, got {:?}",
12279                                self.peek()
12280                            )));
12281                        }
12282                        self.advance();
12283                        let mut values = Vec::new();
12284                        loop {
12285                            values.push(self.parse_expr(0)?);
12286                            match self.peek() {
12287                                Token::Comma => {
12288                                    self.advance();
12289                                }
12290                                Token::RParen => {
12291                                    self.advance();
12292                                    break;
12293                                }
12294                                other => {
12295                                    return Err(self.err(format!(
12296                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12297                                    )));
12298                                }
12299                            }
12300                        }
12301                        if values.is_empty() {
12302                            return Err(
12303                                self.err("FOR VALUES IN requires at least one literal".to_string())
12304                            );
12305                        }
12306                        Ok(PartitionOfBoundsAst::List { values })
12307                    }
12308                    other => Err(self.err(format!(
12309                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12310                    ))),
12311                }
12312            }
12313            other => Err(self.err(format!(
12314                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12315            ))),
12316        }
12317    }
12318
12319    /// v7.16.2 — peek for `information_schema.<tbl>` /
12320    /// `pg_catalog.<tbl>` triples and, if matched, consume all
12321    /// three tokens + return a synthetic table name the engine's
12322    /// SELECT path recognises as a virtual view. Returns `None`
12323    /// when the head doesn't look like a meta-qualified name.
12324    /// Used by `parse_table_ref` to bypass the
12325    /// `expect_ident_like` schema-strip for these specific PG
12326    /// meta schemas (mailrs round-10 A.3).
12327    fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12328        // Extract the schema name. Must be a plain ident token.
12329        let schema = match self.tokens.get(self.pos) {
12330            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12331            _ => return None,
12332        };
12333        // Dot.
12334        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12335            return None;
12336        }
12337        // The table-side ident may lex as a reserved keyword
12338        // (e.g. `Token::Tables`). Tolerate the common ones via a
12339        // helper that reads the trailing token's underlying name.
12340        let tbl = match self.tokens.get(self.pos + 2)? {
12341            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12342            Token::Tables => "tables".to_string(),
12343            // Other PG meta table names that may collide with
12344            // reserved keywords land here as needed.
12345            _ => return None,
12346        };
12347        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12348        // names so the synthetic name doesn't double-prefix
12349        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12350        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12351            ("__spg_info_", tbl.to_ascii_lowercase())
12352        } else if schema.eq_ignore_ascii_case("pg_catalog") {
12353            // v7.39 (round 541) — only the catalogs SPG actually
12354            // synthesises are rewritten, which is what the BARE path
12355            // has always checked. Anything else keeps its own name and
12356            // takes the ordinary route: `pg_stat_activity` and friends
12357            // resolve through meta_view_result, and a name that is no
12358            // catalog at all gets PG's "relation does not exist"
12359            // instead of a message about a view SPG cannot materialise.
12360            let lowered = tbl.to_ascii_lowercase();
12361            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12362                self.advance(); // schema
12363                self.advance(); // dot
12364                self.advance(); // tbl
12365                return Some((lowered.clone(), lowered));
12366            }
12367            let bare = lowered
12368                .strip_prefix("pg_")
12369                .map(alloc::string::String::from)
12370                .unwrap_or(lowered);
12371            ("__spg_pg_", bare)
12372        } else if schema.eq_ignore_ascii_case("mysql") {
12373            // v7.17.0 Phase 3.P0-65 — MySQL system schema
12374            // (`mysql.user`, `mysql.db`). Same synthetic-name
12375            // shape as pg_catalog.
12376            ("__spg_mysql_", tbl.to_ascii_lowercase())
12377        } else {
12378            return None;
12379        };
12380        self.advance(); // schema
12381        self.advance(); // dot
12382        self.advance(); // tbl
12383        Some((
12384            alloc::format!("{prefix}{normalised}"),
12385            tbl.to_ascii_lowercase(),
12386        ))
12387    }
12388
12389    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12390    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12391    /// implicit front of every search_path, so a bare reference to a
12392    /// known catalog table always means the catalog table. Only the
12393    /// names the engine actually synthesises are recognised — any
12394    /// other `pg_*` ident stays a user table (mailrs embed round-12).
12395    fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12396        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12397        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12398        // `pg_catalog` at the front of every search_path. (pg_stat_activity
12399        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12400        // through the meta_view_result path instead, and already resolve
12401        // bare — they must NOT be listed here or the __spg_ rewrite would
12402        // mis-target them.)
12403        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12404        let name = match self.tokens.get(self.pos) {
12405            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12406            _ => return None,
12407        };
12408        // A following dot means this ident is a schema qualifier,
12409        // not a table name — let the qualified path handle it.
12410        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12411            return None;
12412        }
12413        if !PG_META_TABLES.contains(&name.as_str()) {
12414            return None;
12415        }
12416        self.advance();
12417        let bare = name.strip_prefix("pg_").unwrap_or(&name);
12418        Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12419    }
12420
12421    /// Consume a bare ident if its lowercase matches `kw`, else err.
12422    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12423    /// Peeks only; the caller advances.
12424    fn peek_keyword_ident(&self, kw: &str) -> bool {
12425        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12426    }
12427
12428    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12429        match self.advance() {
12430            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12431            other => Err(ParseError {
12432                message: format!("expected {kw:?}, got {other:?}"),
12433                token_pos: self.consumed_pos(),
12434            }),
12435        }
12436    }
12437
12438    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12439    /// literal (`'foo'`) — same shape used by CREATE USER for the
12440    /// username slot.
12441    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12442        match self.advance() {
12443            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12444            other => Err(ParseError {
12445                message: format!("expected identifier or string, got {other:?}"),
12446                token_pos: self.consumed_pos(),
12447            }),
12448        }
12449    }
12450
12451    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12452        match self.advance() {
12453            Token::String(s) => Ok(s),
12454            other => Err(ParseError {
12455                message: format!("expected quoted string, got {other:?}"),
12456                token_pos: self.consumed_pos(),
12457            }),
12458        }
12459    }
12460
12461    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12462        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12463        // subqueries recurse through here without passing
12464        // parse_expr; share the same nesting budget.
12465        self.enter_nested()?;
12466        let r = self.parse_select_stmt_inner();
12467        self.nest_depth -= 1;
12468        r
12469    }
12470
12471    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12472        // Caller dispatches on Token::Select; the inner helper handles
12473        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12474        // get a fresh bare-select parse and may not have their own ORDER
12475        // BY / LIMIT.
12476        let mut head = self.parse_bare_select()?;
12477        let into = self.pending_select_into.take();
12478        self.parse_setop_chain_into(&mut head)?;
12479        self.parse_select_tail_into(&mut head)?;
12480        // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12481        // `CREATE TABLE t AS SELECT …`, which is what a comment in
12482        // `ast.rs` has claimed since v7.38 and what only CTAS actually
12483        // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12484        // to the body, as it does in PostgreSQL.
12485        if let Some((name, temporary)) = into {
12486            return Ok(Statement::CreateMaterializedView(
12487                crate::ast::CreateMaterializedViewStatement {
12488                    temporary,
12489                    name,
12490                    if_not_exists: false,
12491                    columns: Vec::new(),
12492                    body: head,
12493                    with_data: true,
12494                    as_plain_table: true,
12495                },
12496            ));
12497        }
12498        Ok(Statement::Select(head))
12499    }
12500
12501    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12502    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12503    /// token), and INTERSECT [ALL] (a bare ident — it was never
12504    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12505    /// tighter than UNION / EXCEPT — the executor folds the chain
12506    /// left-to-right, which is already correct for LEADING
12507    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12508    /// pair nests into that previous peer, so A UNION B INTERSECT C
12509    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12510    /// groups.
12511    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12512        // A parenthesized group arrives with its own (already
12513        // regrouped) unions on `head`; only the pairs THIS chain
12514        // appends participate in the precedence regroup below —
12515        // nesting an outer INTERSECT into a group-internal peer
12516        // would dissolve the explicit grouping.
12517        let boundary = head.unions.len();
12518        loop {
12519            let base = match self.peek() {
12520                Token::Union => UnionKind::Distinct,
12521                Token::Except => UnionKind::Except,
12522                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12523                _ => break,
12524            };
12525            self.advance();
12526            let kind = if matches!(self.peek(), Token::All) {
12527                self.advance();
12528                match base {
12529                    UnionKind::Distinct => UnionKind::All,
12530                    UnionKind::Except => UnionKind::ExceptAll,
12531                    _ => UnionKind::IntersectAll,
12532                }
12533            } else {
12534                base
12535            };
12536            let peer = self.parse_bare_select()?;
12537            head.unions.push((kind, peer));
12538        }
12539        let mut pairs = core::mem::take(&mut head.unions);
12540        let tail = pairs.split_off(boundary);
12541        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12542        for (kind, peer) in tail {
12543            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12544            // An intersect nests into the previous element of THIS
12545            // chain only; with no new previous element it stays at
12546            // the outer level (the left fold applies it to the
12547            // whole head, group included).
12548            match (
12549                is_intersect,
12550                regrouped.len() > boundary,
12551                regrouped.last_mut(),
12552            ) {
12553                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12554                _ => regrouped.push((kind, peer)),
12555            }
12556        }
12557        head.unions = regrouped;
12558        Ok(())
12559    }
12560
12561    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12562    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12563    /// the top-level bare VALUES statement reuses it verbatim.
12564    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12565    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12566    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12567    /// where the grouping-set universe is still in scope.
12568    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12569        if !matches!(self.peek(), Token::Order) {
12570            return Ok(Vec::new());
12571        }
12572        self.advance();
12573        if !self.peek_is_by() {
12574            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12575        }
12576        self.advance();
12577        let mut keys = Vec::new();
12578        loop {
12579            // v7.39 (round 691) — save/restore, the discipline this parser
12580            // already uses around `pending_sample_preds`, so a subquery inside
12581            // a key neither inherits nor leaks the channel.
12582            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12583            let saved_coll = self.order_key_collation.take();
12584            let parsed = self.parse_expr(0);
12585            self.in_order_by_key = saved_flag;
12586            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12587            let expr = parsed?;
12588            let desc = if matches!(self.peek(), Token::Desc) {
12589                self.advance();
12590                true
12591            } else if matches!(self.peek(), Token::Asc) {
12592                self.advance();
12593                false
12594            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12595                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12596                // one ordering per type, so the btree comparison operators map
12597                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12598                // would need a custom operator class — honest error.
12599                self.advance();
12600                match self.advance() {
12601                    Token::Lt | Token::LtEq => false,
12602                    Token::Gt | Token::GtEq => true,
12603                    other => {
12604                        return Err(self.err(alloc::format!(
12605                            "ORDER BY USING supports the btree comparison \
12606                             operators (< <= > >=); got {other:?}"
12607                        )));
12608                    }
12609                }
12610            } else {
12611                false
12612            };
12613            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12614            let nulls_first = self.parse_optional_nulls_placement()?;
12615            keys.push(OrderBy {
12616                expr,
12617                desc,
12618                nulls_first,
12619                collation,
12620            });
12621            if matches!(self.peek(), Token::Comma) {
12622                self.advance();
12623            } else {
12624                break;
12625            }
12626        }
12627        Ok(keys)
12628    }
12629
12630    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12631        // v7.39 (round 135) — a grouping-set query may have already parsed +
12632        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12633        // no ORDER BY token is present, keep that pre-set order_by rather than
12634        // clobbering it with an empty list.
12635        let parsed_keys = self.parse_order_by_keys()?;
12636        head.order_by = if parsed_keys.is_empty() {
12637            core::mem::take(&mut head.order_by)
12638        } else {
12639            parsed_keys
12640        };
12641        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12642        // order. PG's grammar takes a limit clause and an offset clause
12643        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12644        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12645        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12646        // spelling died on `expected end of input, got Limit`.
12647        //
12648        // Each may appear at most once, and LIMIT and FETCH FIRST are
12649        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12650        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12651        // A second one is left unconsumed here, which the caller reports
12652        // as trailing input rather than silently taking the last.
12653        let mut saw_limit = false;
12654        let mut saw_offset = false;
12655        loop {
12656            if !saw_limit && matches!(self.peek(), Token::Limit) {
12657                self.advance();
12658                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12659                // PG synonyms for "no limit". Treat both as None
12660                // (no head.limit set) so the engine's existing
12661                // unlimited-result path takes over. Reject was the
12662                // pre-5.1 behaviour and broke pg_dump-flavoured
12663                // tooling that occasionally emits LIMIT NULL.
12664                if self.consume_limit_unbounded_sentinel() {
12665                    head.limit = None;
12666                } else {
12667                    let first = self.parse_limit_expr("LIMIT")?;
12668                    // MySQL `LIMIT offset, count` — the first number is
12669                    // the offset when a comma follows.
12670                    if matches!(self.peek(), Token::Comma) {
12671                        self.advance();
12672                        let count = self.parse_limit_expr("LIMIT")?;
12673                        head.offset = Some(first);
12674                        saw_offset = true;
12675                        head.limit = Some(count);
12676                    } else {
12677                        head.limit = Some(first);
12678                    }
12679                }
12680                saw_limit = true;
12681                continue;
12682            }
12683            if !saw_offset && matches!(self.peek(), Token::Offset) {
12684                self.advance();
12685                // PG also accepts an optional `ROW` / `ROWS` trailer
12686                // after the offset value (`OFFSET 10 ROWS`). The
12687                // FETCH-FIRST branch below relies on the same.
12688                let off = self.parse_limit_expr("OFFSET")?;
12689                self.consume_optional_rows_keyword();
12690                head.offset = Some(off);
12691                saw_offset = true;
12692                continue;
12693            }
12694            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12695            // the SQL-standard alias for LIMIT. PG accepts both
12696            // spellings interchangeably; pg_dump emits FETCH FIRST in
12697            // newer versions. We map it onto `head.limit` so the
12698            // engine path is unified.
12699            if !saw_limit
12700                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12701                    if s.eq_ignore_ascii_case("fetch"))
12702            {
12703                self.advance(); // FETCH
12704                // `FIRST` or `NEXT` (both legal per SQL standard).
12705                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12706                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12707                {
12708                    self.advance();
12709                }
12710                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12711                // implicit 1 — but we always consume one if present).
12712                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12713                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12714                {
12715                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12716                    crate::ast::LimitExpr::Literal(1)
12717                } else {
12718                    self.parse_limit_expr("FETCH FIRST")?
12719                };
12720                // Eat `ROW` / `ROWS` if not already consumed above.
12721                self.consume_optional_rows_keyword();
12722                // Optional `ONLY` (the spec form) — or the SQL:2008
12723                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12724                // now honours WITH TIES by extending past the LIMIT
12725                // truncation point through every row that shares the
12726                // last-kept row's ORDER BY key.
12727                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12728                    if s.eq_ignore_ascii_case("only"))
12729                {
12730                    self.advance();
12731                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12732                    if s.eq_ignore_ascii_case("with"))
12733                {
12734                    self.advance(); // WITH
12735                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12736                        if s.eq_ignore_ascii_case("ties"))
12737                    {
12738                        self.advance();
12739                        head.limit_with_ties = true;
12740                    }
12741                }
12742                head.limit = Some(count);
12743                saw_limit = true;
12744                continue;
12745            }
12746            break;
12747        }
12748        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12749        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12750        //       [ OF table_name [, …] ]
12751        //       [ NOWAIT | SKIP LOCKED ]
12752        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12753        // FOR SHARE OF t2`). SPG is a single-writer engine — every
12754        // SELECT already returns a consistent snapshot — so these
12755        // are accept-and-discard: the parser absorbs them so
12756        // mailrs / Rails / Django code paths that emit `SELECT
12757        // … FOR UPDATE` for advisory pessimistic locking load
12758        // without a parser error. The on-disk locking model is
12759        // unchanged; callers that rely on FOR UPDATE for read-
12760        // through-write ordering still get the right answer
12761        // because SPG serialises writes anyway.
12762        head.locking = self
12763            .consume_optional_for_lock_clauses()
12764            .map(alloc::boxed::Box::new);
12765        Ok(())
12766    }
12767
12768    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12769    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12770    /// LOCKED ]` trailers. Each clause is fully accepted and
12771    /// discarded — SPG's single-writer model already satisfies the
12772    /// callers' implicit ordering requirement. Stops at the first
12773    /// token that isn't `FOR`.
12774    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12775        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12776        // not discarded. PG keeps the strongest of several clauses; the
12777        // policy of the last one wins, which is what this loop records.
12778        let mut seen: Option<crate::ast::LockingClause> = None;
12779        while matches!(self.peek(), Token::For) {
12780            // v7.37.14 (A2.5-stub) — record that this query asked
12781            // for a row lock the parser is about to silently
12782            // discard. Operators surface the count via
12783            // `spg_sql::silent_for_update_count()` so they can
12784            // gauge how much of the workload depends on advisory
12785            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12786            // before v7.37.15's per-row tuple locking lands.
12787            crate::record_silent_for_update_clause();
12788            self.advance(); // FOR
12789            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12790            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12791            let mut no_key = false;
12792            let mut key = false;
12793            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12794                if s.eq_ignore_ascii_case("no"))
12795            {
12796                self.advance(); // NO
12797                no_key = true;
12798                // The next ident should be KEY but be generous;
12799                // anything followed by UPDATE/SHARE is accepted.
12800                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12801                    if s.eq_ignore_ascii_case("key"))
12802                {
12803                    self.advance(); // KEY
12804                }
12805            }
12806            // `KEY` prefix (PG `FOR KEY SHARE`).
12807            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12808                if s.eq_ignore_ascii_case("key"))
12809            {
12810                self.advance(); // KEY
12811                key = true;
12812            }
12813            // Lock-strength keyword: UPDATE / SHARE. Required, but
12814            // we're lenient — an unexpected token here just bails
12815            // (we already consumed FOR; caller's downstream
12816            // dispatch will error if anything actually depends on
12817            // the trailing tokens).
12818            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12819                if s.eq_ignore_ascii_case("update"));
12820            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12821                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12822            {
12823                self.advance();
12824                use crate::ast::LockStrength as LS;
12825                let strength = match (is_update, no_key, key) {
12826                    (true, true, _) => LS::NoKeyUpdate,
12827                    (true, _, _) => LS::Update,
12828                    (false, _, true) => LS::KeyShare,
12829                    (false, _, _) => LS::Share,
12830                };
12831                seen = Some(crate::ast::LockingClause {
12832                    strength,
12833                    of_tables: alloc::vec::Vec::new(),
12834                    policy: crate::ast::LockWait::Wait,
12835                });
12836            } else {
12837                // FOR by itself (or `FOR KEY` with nothing after) —
12838                // give up on the lock-clause path. We've already
12839                // advanced past FOR; further attempts to parse
12840                // here would clobber state.
12841                return seen;
12842            }
12843            // Optional `OF tbl[, tbl …]`. mailrs emits this when
12844            // joining and locking only a subset of tables.
12845            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12846                if s.eq_ignore_ascii_case("of"))
12847            {
12848                self.advance(); // OF
12849                #[allow(clippy::while_let_loop)]
12850                loop {
12851                    match self.peek() {
12852                        Token::Ident(_) | Token::QuotedIdent(_) => {
12853                            // v7.39 (round 294) — the name is CAPTURED now: PG
12854                            // validates it against the FROM clause, and an
12855                            // uncaptured list silently means "lock everything".
12856                            let mut nm = match self.advance() {
12857                                Token::Ident(n) | Token::QuotedIdent(n) => n,
12858                                _ => alloc::string::String::new(),
12859                            };
12860                            // Optional schema-qualified `schema.table`.
12861                            if matches!(self.peek(), Token::Dot) {
12862                                self.advance();
12863                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12864                                {
12865                                    self.advance();
12866                                    nm = n;
12867                                }
12868                            }
12869                            if let Some(c) = seen.as_mut() {
12870                                c.of_tables.push(nm);
12871                            }
12872                        }
12873                        _ => break,
12874                    }
12875                    if matches!(self.peek(), Token::Comma) {
12876                        self.advance();
12877                    } else {
12878                        break;
12879                    }
12880                }
12881            }
12882            // Optional `NOWAIT` | `SKIP LOCKED`.
12883            match self.peek().clone() {
12884                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12885                    self.advance();
12886                    if let Some(c) = seen.as_mut() {
12887                        c.policy = crate::ast::LockWait::NoWait;
12888                    }
12889                }
12890                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12891                    self.advance(); // SKIP
12892                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12893                        if s.eq_ignore_ascii_case("locked"))
12894                    {
12895                        self.advance(); // LOCKED
12896                        if let Some(c) = seen.as_mut() {
12897                            c.policy = crate::ast::LockWait::SkipLocked;
12898                        }
12899                    }
12900                }
12901                _ => {}
12902            }
12903            // Loop: PG allows multiple FOR clauses chained.
12904        }
12905        seen
12906    }
12907
12908    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12909    /// Bind value gets resolved during prepared-statement Execute;
12910    /// the Pratt expression parser would over-accept here (e.g.
12911    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12912    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12913    /// sentinel tokens (PG synonyms for "no limit"). Returns true
12914    /// when one was consumed; caller skips the regular
12915    /// limit-value parse and leaves `head.limit` at None.
12916    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12917        if matches!(self.peek(), Token::Null) {
12918            self.advance();
12919            return true;
12920        }
12921        if matches!(self.peek(), Token::All) {
12922            self.advance();
12923            return true;
12924        }
12925        false
12926    }
12927
12928    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12929    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12930    /// SQL-standard shape. No-op when missing.
12931    fn consume_optional_rows_keyword(&mut self) {
12932        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12933            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12934        {
12935            self.advance();
12936        }
12937    }
12938
12939    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12940    ///
12941    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12942    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12943    /// constant, which is why that spelling keeps the token path below.
12944    ///
12945    /// Constants are folded here rather than carried into the tree: the
12946    /// 15+ execution paths that read the row count go through
12947    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12948    /// means "no limit". A clause the engine could not resolve would
12949    /// therefore return the WHOLE table instead of failing. Folding at
12950    /// parse time keeps that impossible; a non-constant clause is still
12951    /// a clean error (recorded residual — closing it wants a resolution
12952    /// pre-pass on the simple-query path, where `substitute_placeholders`
12953    /// does not run).
12954    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12955        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12956        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12957        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12958        // ONLY` both work (its grammar takes a c_expr). Both measured
12959        // against PG 18.4 in round 305.
12960        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12961            return self.parse_limit_constant(label);
12962        }
12963        // One pass, no rewind: `advance()` takes each token by
12964        // `mem::replace`, so a consumed token reads back as Eof and this
12965        // parser cannot backtrack. Everything — bare literal included —
12966        // is therefore folded from the parsed expression rather than
12967        // re-read from the token stream.
12968        let start = self.pos;
12969        let e = self.parse_expr(0)?;
12970        if let crate::ast::Expr::Placeholder(n) = e {
12971            return Ok(crate::ast::LimitExpr::Placeholder(n));
12972        }
12973        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12974        match fold_limit_constant(&e) {
12975            Some(Ok(v)) if v < 0 => Err(ParseError {
12976                message: alloc::format!("{neg_label} must not be negative"),
12977                token_pos: start,
12978            }),
12979            Some(Ok(v)) => u32::try_from(v)
12980                .map(crate::ast::LimitExpr::Literal)
12981                .map_err(|_| ParseError {
12982                    message: alloc::format!("{label} value too large: {v}"),
12983                    token_pos: start,
12984                }),
12985            Some(Err(message)) => Err(ParseError {
12986                message: message.replace("{L}", neg_label),
12987                token_pos: start,
12988            }),
12989            // v7.39 (round 305, V23) — not foldable at parse time
12990            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12991            // expression; the engine evaluates it once before dispatch.
12992            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12993        }
12994    }
12995
12996    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12997        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12998        // coercion rules, not just an integer token: a NUMERIC rounds half
12999        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
13000        // refused with PG's wording ("LIMIT must not be negative", 2201W /
13001        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
13002        // content, failing as an input-syntax error on the value. General
13003        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
13004        // they need an Expr-carrying LimitExpr variant.
13005        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13006        let err_at = |message: alloc::string::String, pos: usize| ParseError {
13007            message,
13008            token_pos: pos,
13009        };
13010        match self.advance() {
13011            Token::Integer(n) if n >= 0 => u32::try_from(n)
13012                .map(crate::ast::LimitExpr::Literal)
13013                .map_err(|_| ParseError {
13014                    message: alloc::format!("{label} value too large: {n}"),
13015                    token_pos: self.consumed_pos(),
13016                }),
13017            Token::Integer(_) => Err(err_at(
13018                alloc::format!("{neg_label} must not be negative"),
13019                self.pos.saturating_sub(1),
13020            )),
13021            Token::Numeric(t) => {
13022                let pos = self.pos.saturating_sub(1);
13023                let v: f64 = t.parse().map_err(|_| {
13024                    err_at(
13025                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13026                        pos,
13027                    )
13028                })?;
13029                if v < 0.0 {
13030                    return Err(err_at(
13031                        alloc::format!("{neg_label} must not be negative"),
13032                        pos,
13033                    ));
13034                }
13035                // Round half away from zero — PG's numeric→bigint cast.
13036                // (no_std: no f64::round; v is non-negative, so truncating
13037                // v + 0.5 is the same thing.)
13038                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
13039                let rounded = (v + 0.5) as u64;
13040                u32::try_from(rounded)
13041                    .map(crate::ast::LimitExpr::Literal)
13042                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
13043            }
13044            Token::Minus => {
13045                let pos = self.pos.saturating_sub(1);
13046                match self.peek() {
13047                    Token::Integer(_) | Token::Numeric(_) => {
13048                        self.advance();
13049                        Err(err_at(
13050                            alloc::format!("{neg_label} must not be negative"),
13051                            pos,
13052                        ))
13053                    }
13054                    other => Err(err_at(
13055                        alloc::format!(
13056                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13057                        ),
13058                        pos,
13059                    )),
13060                }
13061            }
13062            Token::String(t) => {
13063                let pos = self.pos.saturating_sub(1);
13064                match t.trim().parse::<i64>() {
13065                    Ok(n) if n < 0 => Err(err_at(
13066                        alloc::format!("{neg_label} must not be negative"),
13067                        pos,
13068                    )),
13069                    Ok(n) => u32::try_from(n)
13070                        .map(crate::ast::LimitExpr::Literal)
13071                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
13072                    Err(_) => Err(err_at(
13073                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13074                        pos,
13075                    )),
13076                }
13077            }
13078            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
13079            other => Err(ParseError {
13080                message: alloc::format!(
13081                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13082                ),
13083                token_pos: self.consumed_pos(),
13084            }),
13085        }
13086    }
13087
13088    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
13089    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
13090    /// `unions` empty and `order_by` / `limit` `None`; the top-level
13091    /// `parse_select_stmt` is responsible for filling those in.
13092    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
13093    /// call in the expression tree to the per-set integer bitmask
13094    /// (PG semantics: one bit per argument, MSB first; 1 = the key
13095    /// is dropped in this grouping set). Runs during the ROLLUP /
13096    /// CUBE / GROUPING SETS expansion, where the set is known.
13097    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
13098    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
13099    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
13100        if let Expr::FunctionCall { name, .. } = expr
13101            && name.eq_ignore_ascii_case("grouping")
13102        {
13103            if !out.iter().any(|e| e == expr) {
13104                out.push(expr.clone());
13105            }
13106            return;
13107        }
13108        match expr {
13109            Expr::Binary { lhs, rhs, .. } => {
13110                Self::collect_grouping_calls(lhs, out);
13111                Self::collect_grouping_calls(rhs, out);
13112            }
13113            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13114                Self::collect_grouping_calls(expr, out)
13115            }
13116            Expr::FunctionCall { args, .. } => {
13117                for a in args {
13118                    Self::collect_grouping_calls(a, out);
13119                }
13120            }
13121            Expr::Case {
13122                operand,
13123                branches,
13124                else_branch,
13125            } => {
13126                if let Some(o) = operand {
13127                    Self::collect_grouping_calls(o, out);
13128                }
13129                for (c, v) in branches {
13130                    Self::collect_grouping_calls(c, out);
13131                    Self::collect_grouping_calls(v, out);
13132                }
13133                if let Some(x) = else_branch {
13134                    Self::collect_grouping_calls(x, out);
13135                }
13136            }
13137            _ => {}
13138        }
13139    }
13140
13141    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
13142    /// `grp_exprs[k]` with a reference to the synthetic ordering column
13143    /// `__grp_ord_k` (injected per grouping-set branch).
13144    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
13145        if let Expr::FunctionCall { name, .. } = expr
13146            && name.eq_ignore_ascii_case("grouping")
13147        {
13148            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13149                *expr = Expr::Column(crate::ast::ColumnName {
13150                    qualifier: None,
13151                    name: alloc::format!("__grp_ord_{k}"),
13152                });
13153            }
13154            return;
13155        }
13156        match expr {
13157            Expr::Binary { lhs, rhs, .. } => {
13158                Self::rewrite_grouping_to_col(lhs, grp_exprs);
13159                Self::rewrite_grouping_to_col(rhs, grp_exprs);
13160            }
13161            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13162                Self::rewrite_grouping_to_col(expr, grp_exprs)
13163            }
13164            Expr::FunctionCall { args, .. } => {
13165                for a in args {
13166                    Self::rewrite_grouping_to_col(a, grp_exprs);
13167                }
13168            }
13169            Expr::Case {
13170                operand,
13171                branches,
13172                else_branch,
13173            } => {
13174                if let Some(o) = operand {
13175                    Self::rewrite_grouping_to_col(o, grp_exprs);
13176                }
13177                for (c, v) in branches {
13178                    Self::rewrite_grouping_to_col(c, grp_exprs);
13179                    Self::rewrite_grouping_to_col(v, grp_exprs);
13180                }
13181                if let Some(x) = else_branch {
13182                    Self::rewrite_grouping_to_col(x, grp_exprs);
13183                }
13184            }
13185            _ => {}
13186        }
13187    }
13188
13189    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13190    /// as the list of key sets it contributes. A bare expression is one
13191    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13192    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13193    /// the concatenation of its items' sets, where an item is itself an
13194    /// element, a parenthesized key list, or the empty set `()`. A
13195    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13196    /// move together.
13197    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13198        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13199        // ROLLUP ( … ) / CUBE ( … )
13200        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13201            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13202        {
13203            let is_cube = is_kw(self.peek(), "cube");
13204            self.advance(); // ROLLUP / CUBE
13205            self.advance(); // (
13206            let mut units: Vec<Vec<Expr>> = Vec::new();
13207            loop {
13208                if matches!(self.peek(), Token::LParen) {
13209                    // Composite unit: (a, b) rolls up as one.
13210                    self.advance();
13211                    let mut unit = Vec::new();
13212                    if !matches!(self.peek(), Token::RParen) {
13213                        loop {
13214                            unit.push(self.parse_expr(0)?);
13215                            match self.peek() {
13216                                Token::Comma => {
13217                                    self.advance();
13218                                }
13219                                Token::RParen => break,
13220                                other => {
13221                                    return Err(self.err(format!(
13222                                        "expected ',' or ')' in grouping unit, got {other:?}"
13223                                    )));
13224                                }
13225                            }
13226                        }
13227                    }
13228                    self.advance(); // )
13229                    units.push(unit);
13230                } else {
13231                    units.push(alloc::vec![self.parse_expr(0)?]);
13232                }
13233                match self.peek() {
13234                    Token::Comma => {
13235                        self.advance();
13236                    }
13237                    Token::RParen => break,
13238                    other => {
13239                        return Err(self.err(format!(
13240                            "expected ',' or ')' in grouping list, got {other:?}"
13241                        )));
13242                    }
13243                }
13244            }
13245            self.advance(); // )
13246            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13247                units
13248                    .iter()
13249                    .zip(unit_sel.iter())
13250                    .filter(|(_, keep)| **keep)
13251                    .flat_map(|(u, _)| u.iter().cloned())
13252                    .collect()
13253            };
13254            let n = units.len();
13255            if is_cube {
13256                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13257                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13258                    .collect();
13259                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13260                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13261            }
13262            return Ok((0..=n)
13263                .rev()
13264                .map(|keep| {
13265                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13266                    flatten(&sel)
13267                })
13268                .collect());
13269        }
13270        // GROUPING SETS ( item [, item]* )
13271        if is_kw(self.peek(), "grouping")
13272            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13273        {
13274            self.advance(); // GROUPING
13275            self.advance(); // SETS
13276            if !matches!(self.peek(), Token::LParen) {
13277                return Err(self.err(format!(
13278                    "expected '(' after GROUPING SETS, got {:?}",
13279                    self.peek()
13280                )));
13281            }
13282            self.advance(); // outer (
13283            let mut sets: Vec<Vec<Expr>> = Vec::new();
13284            loop {
13285                if matches!(self.peek(), Token::LParen) {
13286                    // A parenthesized key list (or the empty set).
13287                    self.advance();
13288                    let mut set = Vec::new();
13289                    if !matches!(self.peek(), Token::RParen) {
13290                        loop {
13291                            set.push(self.parse_expr(0)?);
13292                            match self.peek() {
13293                                Token::Comma => {
13294                                    self.advance();
13295                                }
13296                                Token::RParen => break,
13297                                other => {
13298                                    return Err(self.err(format!(
13299                                        "expected ',' or ')' in grouping set, got {other:?}"
13300                                    )));
13301                                }
13302                            }
13303                        }
13304                    }
13305                    self.advance(); // )
13306                    sets.push(set);
13307                } else {
13308                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13309                    // bare expression.
13310                    sets.extend(self.parse_grouping_element()?);
13311                }
13312                match self.peek() {
13313                    Token::Comma => {
13314                        self.advance();
13315                    }
13316                    Token::RParen => break,
13317                    other => {
13318                        return Err(self.err(format!(
13319                            "expected ',' or ')' after a grouping set, got {other:?}"
13320                        )));
13321                    }
13322                }
13323            }
13324            self.advance(); // outer )
13325            return Ok(sets);
13326        }
13327        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13328    }
13329
13330    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13331        // v7.38 (read01) — a reference to a key that is dropped in this grouping
13332        // set evaluates to NULL, at any depth. Previously only a *top-level*
13333        // select item equal to a dropped key was nullified, so a key nested in
13334        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13335        // column and failed to resolve against the set's synthetic schema.
13336        if dropped.iter().any(|d| d == expr) {
13337            *expr = Expr::Literal(Literal::Null);
13338            return;
13339        }
13340        if let Expr::FunctionCall { name, args } = expr
13341            && name.eq_ignore_ascii_case("grouping")
13342        {
13343            let mut mask: i64 = 0;
13344            for a in args.iter() {
13345                mask <<= 1;
13346                if dropped.iter().any(|d| d == a) {
13347                    mask |= 1;
13348                }
13349            }
13350            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13351            // literal: a bare integer in a select item is indistinguishable
13352            // from a positional reference once `ORDER BY 1` substitutes the
13353            // item back in, and the round-232 position check then read the
13354            // mask value as an out-of-range position. The cast changes
13355            // nothing semantically (grouping() is integer).
13356            *expr = Expr::Cast {
13357                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13358                target: crate::ast::CastTarget::Int,
13359            };
13360            return;
13361        }
13362        // Generic recursion over the common expression shapes the
13363        // SELECT list uses; anything without child expressions is
13364        // left alone.
13365        match expr {
13366            Expr::FunctionCall { args, .. } => {
13367                for a in args {
13368                    Self::substitute_grouping_calls(a, dropped);
13369                }
13370            }
13371            Expr::Binary { lhs, rhs, .. } => {
13372                Self::substitute_grouping_calls(lhs, dropped);
13373                Self::substitute_grouping_calls(rhs, dropped);
13374            }
13375            Expr::Unary { expr: inner, .. } => {
13376                Self::substitute_grouping_calls(inner, dropped);
13377            }
13378            Expr::Cast { expr: inner, .. } => {
13379                Self::substitute_grouping_calls(inner, dropped);
13380            }
13381            Expr::Case {
13382                operand,
13383                branches,
13384                else_branch,
13385            } => {
13386                if let Some(op) = operand {
13387                    Self::substitute_grouping_calls(op, dropped);
13388                }
13389                for (w, t) in branches {
13390                    Self::substitute_grouping_calls(w, dropped);
13391                    Self::substitute_grouping_calls(t, dropped);
13392                }
13393                if let Some(e) = else_branch {
13394                    Self::substitute_grouping_calls(e, dropped);
13395                }
13396            }
13397            // v7.38 (read01) — recurse into the remaining child-bearing shapes
13398            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13399            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13400            // …` is the canonical rollup-total label idiom).
13401            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13402            Expr::Like { expr, pattern, .. } => {
13403                Self::substitute_grouping_calls(expr, dropped);
13404                Self::substitute_grouping_calls(pattern, dropped);
13405            }
13406            Expr::InList { expr, list, .. } => {
13407                Self::substitute_grouping_calls(expr, dropped);
13408                for item in list {
13409                    Self::substitute_grouping_calls(item, dropped);
13410                }
13411            }
13412            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13413            Expr::Array(items) => {
13414                for item in items {
13415                    Self::substitute_grouping_calls(item, dropped);
13416                }
13417            }
13418            Expr::ArraySubscript { target, index } => {
13419                Self::substitute_grouping_calls(target, dropped);
13420                Self::substitute_grouping_calls(index, dropped);
13421            }
13422            Expr::ArraySlice { target, lo, hi } => {
13423                Self::substitute_grouping_calls(target, dropped);
13424                if let Some(lo) = lo {
13425                    Self::substitute_grouping_calls(lo, dropped);
13426                }
13427                if let Some(hi) = hi {
13428                    Self::substitute_grouping_calls(hi, dropped);
13429                }
13430            }
13431            Expr::AnyAll { expr, array, .. } => {
13432                Self::substitute_grouping_calls(expr, dropped);
13433                Self::substitute_grouping_calls(array, dropped);
13434            }
13435            _ => {}
13436        }
13437    }
13438
13439    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13440        // v7.37.17 (17.6 siblings) — parenthesized set-operation
13441        // group: `( <select chain> )` usable anywhere a query block
13442        // is (head or peer of an outer chain). The group's own
13443        // unions ride the returned SelectStatement; the executor's
13444        // nested-peer recursion runs them.
13445        if matches!(self.peek(), Token::LParen)
13446            && matches!(
13447                self.tokens.get(self.pos + 1),
13448                Some(Token::Select | Token::LParen | Token::Values)
13449            )
13450        {
13451            self.advance(); // (
13452            self.enter_nested()?;
13453            // v7.37 D.20 — a group whose head is a VALUES list:
13454            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13455            // otherwise recurse into a nested SELECT/group head.
13456            let mut head = (if matches!(self.peek(), Token::Values) {
13457                self.advance(); // VALUES
13458                self.parse_values_rows_body()
13459            } else {
13460                self.parse_bare_select()
13461            })
13462            .and_then(|mut h| {
13463                self.parse_setop_chain_into(&mut h)?;
13464                Ok(h)
13465            });
13466            self.nest_depth -= 1;
13467            let mut head = match &mut head {
13468                Ok(h) => core::mem::take(h),
13469                Err(_) => return head,
13470            };
13471            // v7.37.17 (17.6 siblings) — group-internal tail:
13472            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13473            // group head, then wrap the group as a derived table
13474            // (SELECT * FROM (group)) so the outer chain / outer
13475            // tail can't clobber the group's own ordering or limit.
13476            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13477                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13478                    if s.eq_ignore_ascii_case("fetch"));
13479            if has_tail {
13480                self.parse_select_tail_into(&mut head)?;
13481                head = SelectStatement {
13482                    locking: None,
13483                    ctes: Vec::new(),
13484                    distinct: false,
13485                    distinct_on: Vec::new(),
13486                    items: alloc::vec![SelectItem::Wildcard],
13487                    from: Some(FromClause {
13488                        primary: TableRef {
13489                            name: "subquery".to_string(),
13490                            alias: None,
13491                            only: false,
13492                            as_of_segment: None,
13493                            unnest_expr: None,
13494                            unnest_column_aliases: Vec::new(),
13495                            with_ordinality: false,
13496                            generate_series_args: None,
13497                            lateral_subquery: Some(Box::new(head)),
13498                            jsonb_each_text_arg: None,
13499                            table_fn_call: None,
13500                            rows_from: None,
13501                            json_table: None,
13502                            scalar_fn_item: false,
13503                        },
13504                        joins: Vec::new(),
13505                    }),
13506                    where_: None,
13507                    group_by: None,
13508                    group_by_all: false,
13509                    having: None,
13510                    unions: Vec::new(),
13511                    order_by: Vec::new(),
13512                    limit: None,
13513                    offset: None,
13514                    limit_with_ties: false,
13515                    window_check_exprs: Vec::new(),
13516                };
13517            }
13518            if !matches!(self.peek(), Token::RParen) {
13519                return Err(self.err(format!(
13520                    "expected ')' after parenthesized query group, got {:?}",
13521                    self.peek()
13522                )));
13523            }
13524            self.advance();
13525            return Ok(head);
13526        }
13527        // `TABLE name` shorthand as a query block — valid anywhere
13528        // a SELECT head is (set-op peers included).
13529        if matches!(self.peek(), Token::Table)
13530            && matches!(
13531                self.tokens.get(self.pos + 1),
13532                Some(Token::Ident(_) | Token::QuotedIdent(_))
13533            )
13534        {
13535            return self.parse_table_shorthand();
13536        }
13537        if !matches!(self.peek(), Token::Select) {
13538            return Err(self.err(format!(
13539                "expected SELECT to start a query block, got {:?}",
13540                self.peek()
13541            )));
13542        }
13543        self.advance();
13544        let distinct = if matches!(self.peek(), Token::Distinct) {
13545            self.advance();
13546            true
13547        } else {
13548            false
13549        };
13550        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13551        // keep the first row (per ORDER BY) of each group the
13552        // expressions define. Django's .distinct('field') shape.
13553        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13554            self.advance(); // ON
13555            if !matches!(self.peek(), Token::LParen) {
13556                return Err(self.err(format!(
13557                    "expected '(' after DISTINCT ON, got {:?}",
13558                    self.peek()
13559                )));
13560            }
13561            self.advance();
13562            let mut exprs = Vec::new();
13563            loop {
13564                exprs.push(self.parse_expr(0)?);
13565                match self.peek() {
13566                    Token::Comma => {
13567                        self.advance();
13568                    }
13569                    Token::RParen => break,
13570                    other => {
13571                        return Err(self.err(format!(
13572                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13573                        )));
13574                    }
13575                }
13576            }
13577            self.advance(); // )
13578            exprs
13579        } else {
13580            Vec::new()
13581        };
13582        let mut items = self.parse_select_list()?;
13583        // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13584        // of CTAS. It sits exactly here in PG's grammar, right after the
13585        // target list.
13586        //
13587        // A comment in `ast.rs` has said since v7.38 that CTAS and
13588        // `SELECT INTO` lower to the same node. Only CTAS ever did:
13589        // `SELECT i INTO t FROM src` answered `syntax error at or near
13590        // "INTO"`, which the differential found while measuring what
13591        // PostgreSQL tags each of the five materialising forms with. A
13592        // comment describing a capability the code does not have is the
13593        // defect this version has been finding all day, and this is the
13594        // one it found in the parser.
13595        //
13596        // `INTO` is captured rather than consumed here: the name has to
13597        // travel out of a function that returns a `SelectStatement`, and
13598        // the caller lowers the whole thing to the CTAS node.
13599        if matches!(self.peek(), Token::Into) {
13600            self.advance();
13601            // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13602            // the target, not part of its name. SPG has one storage
13603            // class, so `UNLOGGED` is accepted and means nothing, which
13604            // is what it already means on `CREATE TABLE`.
13605            let mut temporary = false;
13606            loop {
13607                match self.peek().clone() {
13608                    Token::Ident(w) | Token::QuotedIdent(w)
13609                        if w.eq_ignore_ascii_case("temp")
13610                            || w.eq_ignore_ascii_case("temporary") =>
13611                    {
13612                        temporary = true;
13613                        self.advance();
13614                    }
13615                    Token::Ident(w) | Token::QuotedIdent(w)
13616                        if w.eq_ignore_ascii_case("unlogged") =>
13617                    {
13618                        self.advance();
13619                    }
13620                    Token::Table => {
13621                        self.advance();
13622                    }
13623                    _ => break,
13624                }
13625            }
13626            let name = match self.peek().clone() {
13627                Token::Ident(w) | Token::QuotedIdent(w) => {
13628                    self.advance();
13629                    w
13630                }
13631                other => {
13632                    return Err(self.err(alloc::format!(
13633                        "expected a table name after SELECT … INTO, got {other:?}"
13634                    )));
13635                }
13636            };
13637            self.pending_select_into = Some((name, temporary));
13638        }
13639        // Scope the TABLESAMPLE lowering channel to this SELECT:
13640        // stash whatever an enclosing select accumulated, collect
13641        // our own FROM's predicates, restore after the combine.
13642        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13643        let mut from = if matches!(self.peek(), Token::From) {
13644            self.advance();
13645            Some(self.parse_from_clause()?)
13646        } else {
13647            None
13648        };
13649        // v7.37 D.22 — a set-returning function in the projection with no FROM
13650        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13651        // rows. Move the first SRF projection item to a FROM-position derived
13652        // table and replace it in the projection with a reference to its output
13653        // column; sibling scalar columns repeat per SRF row. PG names the output
13654        // column after the function (or its AS alias). Reuses the FROM-SRF
13655        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13656        // works via the targetlist-SRF path.
13657        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13658        // `SELECT * FROM f(args)` — the record's fields become the columns, which
13659        // is exactly what the function's own row shape already is. Anywhere else
13660        // (per outer row, or beside other items) it would need a real record-typed
13661        // projection, so it says so rather than answering something else.
13662        if let [
13663            SelectItem::Expr {
13664                expr: Expr::FunctionCall { name, args },
13665                ..
13666            },
13667        ] = items.as_slice()
13668            && name == "__record_expand"
13669        {
13670            let Some(Expr::FunctionCall {
13671                name: inner_name,
13672                args: inner_args,
13673            }) = args.first()
13674            else {
13675                return Err(self.err(
13676                    "(<expr>).* expands a function's record — it needs a function call".into(),
13677                ));
13678            };
13679            if from.is_some() {
13680                return Err(self.err(
13681                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13682                        .into(),
13683                ));
13684            }
13685            let fn_ref = TableRef {
13686                name: inner_name.clone(),
13687                alias: None,
13688                only: false,
13689                as_of_segment: None,
13690                unnest_expr: None,
13691                unnest_column_aliases: Vec::new(),
13692                with_ordinality: false,
13693                generate_series_args: None,
13694                lateral_subquery: None,
13695                jsonb_each_text_arg: None,
13696                table_fn_call: Some(Box::new((
13697                    inner_name.to_ascii_lowercase(),
13698                    inner_args.clone(),
13699                ))),
13700                rows_from: None,
13701                json_table: None,
13702                scalar_fn_item: false,
13703            };
13704            items = alloc::vec![SelectItem::Wildcard];
13705            from = Some(FromClause {
13706                primary: fn_ref,
13707                joins: Vec::new(),
13708            });
13709        }
13710        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13711        // FROM, keeps its marker: the ENGINE lowers it, because naming the
13712        // record's fields takes the catalog. It becomes a LATERAL of the same
13713        // function plus one item per declared column — the machinery rounds 65
13714        // and 69 already built.
13715        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13716        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13717        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13718        // express, since the lifted one becomes a scan and the other would
13719        // expand per its rows (a cross product, not a zip). So when the
13720        // projection holds more than one top-level function call, the lift steps
13721        // aside and the engine's target-list expansion takes the whole list.
13722        let fn_call_items = items
13723            .iter()
13724            .filter(|it| {
13725                matches!(
13726                    it,
13727                    SelectItem::Expr {
13728                        expr: Expr::FunctionCall { .. },
13729                        ..
13730                    }
13731                )
13732            })
13733            .count();
13734        if from.is_none() && fn_call_items <= 1 {
13735            let mut found: Option<(usize, TableRef, String)> = None;
13736            for (i, item) in items.iter().enumerate() {
13737                if let SelectItem::Expr {
13738                    expr: Expr::FunctionCall { name, args },
13739                    alias,
13740                } = item
13741                {
13742                    let lname = name.to_ascii_lowercase();
13743                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
13744                    let (unnest, gs) = match lname.as_str() {
13745                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13746                        "generate_series" if (2..=3).contains(&args.len()) => {
13747                            (None, Some(args.clone()))
13748                        }
13749                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
13750                        // no-FROM projection yields the 1-based subscripts, i.e.
13751                        // generate_series(1, array_length(arr, dim)); an invalid
13752                        // dimension makes array_length NULL → 0 rows, as in PG.
13753                        "generate_subscripts" if args.len() == 2 => (
13754                            None,
13755                            Some(alloc::vec![
13756                                Expr::Literal(Literal::Integer(1)),
13757                                Expr::FunctionCall {
13758                                    name: "array_length".to_string(),
13759                                    args: args.clone(),
13760                                },
13761                            ]),
13762                        ),
13763                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13764                        // in a no-FROM projection unnest their *_to_array form.
13765                        "string_to_table" | "regexp_split_to_table" => {
13766                            let array_fn = if lname == "string_to_table" {
13767                                "string_to_array"
13768                            } else {
13769                                "regexp_split_to_array"
13770                            };
13771                            (
13772                                Some(Box::new(Expr::FunctionCall {
13773                                    name: array_fn.to_string(),
13774                                    args: args.clone(),
13775                                })),
13776                                None,
13777                            )
13778                        }
13779                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13780                        // a no-FROM projection expand per element. The scalar form
13781                        // returns the elements as a TEXT array, so unnest over the
13782                        // same call materialises one row each (same rewrite the
13783                        // FROM-clause form uses).
13784                        "jsonb_array_elements"
13785                        | "json_array_elements"
13786                        | "jsonb_array_elements_text"
13787                        | "json_array_elements_text"
13788                            if args.len() == 1 =>
13789                        {
13790                            (
13791                                Some(Box::new(Expr::FunctionCall {
13792                                    name: lname.clone(),
13793                                    args: args.clone(),
13794                                })),
13795                                None,
13796                            )
13797                        }
13798                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13799                        // in a no-FROM projection expands per match (scalar form
13800                        // returns the matches as a TEXT array → unnest).
13801                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13802                            Some(Box::new(Expr::FunctionCall {
13803                                name: lname.clone(),
13804                                args: args.clone(),
13805                            })),
13806                            None,
13807                        ),
13808                        _ => continue,
13809                    };
13810                    found = Some((
13811                        i,
13812                        TableRef {
13813                            name: colname.clone(),
13814                            alias: Some(colname.clone()),
13815                            only: false,
13816                            as_of_segment: None,
13817                            unnest_expr: unnest,
13818                            unnest_column_aliases: alloc::vec![colname.clone()],
13819                            with_ordinality: false,
13820                            generate_series_args: gs,
13821                            lateral_subquery: None,
13822                            jsonb_each_text_arg: None,
13823                            table_fn_call: None,
13824                            rows_from: None,
13825                            json_table: None,
13826                            scalar_fn_item: false,
13827                        },
13828                        colname,
13829                    ));
13830                    break;
13831                }
13832            }
13833            if let Some((idx, tref, colname)) = found {
13834                from = Some(FromClause {
13835                    primary: tref,
13836                    joins: Vec::new(),
13837                });
13838                items[idx] = SelectItem::Expr {
13839                    expr: Expr::Column(ColumnName {
13840                        qualifier: None,
13841                        name: colname.clone(),
13842                    }),
13843                    alias: Some(colname),
13844                };
13845            }
13846        }
13847        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13848        let where_ = if matches!(self.peek(), Token::Where) {
13849            self.advance();
13850            Some(self.parse_expr(0)?)
13851        } else {
13852            None
13853        };
13854        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13855            Some(match acc {
13856                Some(w) => Expr::Binary {
13857                    lhs: Box::new(pred),
13858                    op: crate::ast::BinOp::And,
13859                    rhs: Box::new(w),
13860                },
13861                None => pred,
13862            })
13863        });
13864        self.pending_sample_preds = enclosing_sample_preds;
13865        let mut group_by_all = false;
13866        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13867        // share one expansion: `grouping_sets` lists the key subsets
13868        // (first = primary, assigned to stmt.group_by; the rest
13869        // become UNION ALL peers), `grouping_universe` is the full
13870        // key list used to compute each peer's dropped keys.
13871        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13872        let mut grouping_universe: Vec<Expr> = Vec::new();
13873        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13874        // A BOOL, not the key list: this frame is the statement parser's, and
13875        // round 430 measured that a `Vec` local here is enough on its own to
13876        // tip the 512 KiB nesting guard. The keys are recoverable from
13877        // `grouping_universe`, which a rollup fills with exactly them.
13878        let mut mysql_rollup = false;
13879        let group_by = if matches!(self.peek(), Token::Group) {
13880            self.advance();
13881            if !self.peek_is_by() {
13882                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13883            }
13884            self.advance();
13885            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13886            // every non-aggregate SELECT-list item later.
13887            if matches!(self.peek(), Token::All) {
13888                self.advance();
13889                group_by_all = true;
13890                None
13891            } else {
13892                // v7.39 (round 242) — PG's general grouping-element grammar:
13893                // GROUP BY [DISTINCT] element [, element]*, where an element
13894                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13895                // SETS (…) — mixed freely. Each element yields a list of
13896                // key sets; the query's grouping sets are the CARTESIAN
13897                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13898                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13899                // content. ROLLUP/CUBE members may be composite
13900                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13901                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13902                // parser handled only a lone ROLLUP/CUBE/GS as the whole
13903                // clause.
13904                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13905                    self.advance();
13906                    true
13907                } else {
13908                    false
13909                };
13910                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13911                loop {
13912                    element_sets.push(self.parse_grouping_element()?);
13913                    if matches!(self.peek(), Token::Comma) {
13914                        self.advance();
13915                    } else {
13916                        break;
13917                    }
13918                }
13919                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13920                for el in &element_sets {
13921                    let mut next: Vec<Vec<Expr>> = Vec::new();
13922                    for base in &total {
13923                        for set in el {
13924                            let mut merged = base.clone();
13925                            for k in set {
13926                                if !merged.iter().any(|m| m == k) {
13927                                    merged.push(k.clone());
13928                                }
13929                            }
13930                            next.push(merged);
13931                        }
13932                    }
13933                    total = next;
13934                }
13935                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13936                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13937                // The keys and the aggregates come out identical; the ROW
13938                // ORDER does not, and that is the part a report depends on.
13939                // MySQL interleaves each group's subtotal right after its
13940                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13941                // where the union-of-grouping-sets expansion emits every
13942                // leaf first and then every subtotal. MariaDB REFUSES an
13943                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13944                // order itself — measured on MariaDB 11 and MySQL 9.7, which
13945                // agree on the order and disagree only on whether ORDER BY
13946                // is allowed (MySQL allows it; SPG allows it too, since
13947                // refusing would break the clients that can write it).
13948                if self.mysql_dialect
13949                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13950                    && matches!(
13951                        self.tokens.get(self.pos + 1),
13952                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13953                    )
13954                {
13955                    self.advance(); // WITH
13956                    self.advance(); // ROLLUP
13957                    let keys = total.into_iter().next().unwrap_or_default();
13958                    mysql_rollup = true;
13959                    // n+1 prefixes, largest first — the same expansion
13960                    // `ROLLUP (…)` produces.
13961                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13962                }
13963                if distinct_sets {
13964                    let mut seen: Vec<Vec<String>> = Vec::new();
13965                    total.retain(|set| {
13966                        let mut key: Vec<String> =
13967                            set.iter().map(|e| alloc::format!("{e}")).collect();
13968                        key.sort();
13969                        if seen.contains(&key) {
13970                            false
13971                        } else {
13972                            seen.push(key);
13973                            true
13974                        }
13975                    });
13976                }
13977                if total.len() > 1 {
13978                    let mut universe: Vec<Expr> = Vec::new();
13979                    for set in &total {
13980                        for k in set {
13981                            if !universe.iter().any(|u| u == k) {
13982                                universe.push(k.clone());
13983                            }
13984                        }
13985                    }
13986                    grouping_universe = universe;
13987                    let primary = total[0].clone();
13988                    grouping_sets = total;
13989                    Some(primary)
13990                } else {
13991                    // One set (a plain GROUP BY list, or a single-set
13992                    // spelling like GROUPING SETS ((a, b))). An EMPTY
13993                    // single set — GROUPING SETS (()) — stays
13994                    // `Some(vec![])`: the grand-total group, which must
13995                    // run the aggregate path.
13996                    Some(total.into_iter().next().unwrap_or_default())
13997                }
13998            }
13999        } else {
14000            None
14001        };
14002        let having = if matches!(self.peek(), Token::Having) {
14003            self.advance();
14004            Some(self.parse_expr(0)?)
14005        } else {
14006            None
14007        };
14008        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
14009        // OVER w parsed to a marker above; inline each definition
14010        // into the referencing WindowFunction nodes.
14011        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
14012        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
14013            self.advance();
14014            loop {
14015                let wname = self.expect_ident_like()?;
14016                if !matches!(self.peek(), Token::As) {
14017                    return Err(self.err(format!(
14018                        "expected AS after WINDOW {wname}, got {:?}",
14019                        self.peek()
14020                    )));
14021                }
14022                self.advance();
14023                // v7.39 (round 229) — PG rejects a redefinition outright.
14024                if window_defs
14025                    .iter()
14026                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
14027                {
14028                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
14029                }
14030                let def = self.parse_over_clause()?;
14031                // A definition may itself copy an earlier one
14032                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
14033                // so resolve it against the defs already in scope. Same
14034                // copy rules as an `OVER (w1 …)` in the select list.
14035                let mut probe = Expr::WindowFunction {
14036                    name: String::new(),
14037                    args: Vec::new(),
14038                    partition_by: def.0,
14039                    order_by: def.1,
14040                    frame: def.2,
14041                    null_treatment: crate::ast::NullTreatment::Respect,
14042                    filter: None,
14043                };
14044                Self::substitute_named_windows(&mut probe, &window_defs)
14045                    .map_err(|m| self.err(m))?;
14046                let Expr::WindowFunction {
14047                    partition_by,
14048                    order_by,
14049                    frame,
14050                    ..
14051                } = probe
14052                else {
14053                    unreachable!("probe is a WindowFunction")
14054                };
14055                window_defs.push((wname, (partition_by, order_by, frame)));
14056                if matches!(self.peek(), Token::Comma) {
14057                    self.advance();
14058                    continue;
14059                }
14060                break;
14061            }
14062        }
14063        // v7.39 (round 705) — which definitions did anything reference?
14064        // The ones nothing did used to be dropped here, unexamined, so
14065        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
14066        // definition whether referenced or not. Their key expressions ride
14067        // out on the statement for the engine to resolve.
14068        let mut window_refs: Vec<String> = Vec::new();
14069        if !window_defs.is_empty() {
14070            for it in &items {
14071                if let SelectItem::Expr { expr, .. } = it {
14072                    Self::collect_named_window_refs(expr, &mut window_refs);
14073                }
14074            }
14075        }
14076        let window_check_exprs: Vec<Expr> = window_defs
14077            .iter()
14078            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
14079            .flat_map(|(_, (partition, order, _))| {
14080                partition
14081                    .iter()
14082                    .cloned()
14083                    .chain(order.iter().map(|(e, _, _)| e.clone()))
14084            })
14085            .collect();
14086        if !window_defs.is_empty()
14087            || items
14088                .iter()
14089                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
14090        {
14091            for it in &mut items {
14092                if let SelectItem::Expr { expr, .. } = it {
14093                    Self::substitute_named_windows(expr, &window_defs)
14094                        .map_err(|m| self.err(m))?;
14095                }
14096            }
14097        }
14098        // `GROUP BY 1` — positional keys substitute with the Nth
14099        // select item's expression (same contract ORDER BY has had
14100        // since v6.x). Out-of-range positions error.
14101        let group_by = match group_by {
14102            Some(mut keys) => {
14103                for k in &mut keys {
14104                    if let Expr::Literal(Literal::Integer(n)) = k {
14105                        let idx = *n;
14106                        if idx < 1 || idx as usize > items.len() {
14107                            return Err(self.err(alloc::format!(
14108                                "GROUP BY position {idx} is not in select list"
14109                            )));
14110                        }
14111                        match &items[(idx - 1) as usize] {
14112                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
14113                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
14114                                return Err(self.err(alloc::format!(
14115                                    "GROUP BY position {idx} references a wildcard item"
14116                                )));
14117                            }
14118                        }
14119                    }
14120                }
14121                Some(keys)
14122            }
14123            None => None,
14124        };
14125        let mut stmt = SelectStatement {
14126            locking: None,
14127            ctes: Vec::new(),
14128            distinct,
14129            distinct_on,
14130            items,
14131            from,
14132            where_,
14133            group_by,
14134            group_by_all,
14135            having,
14136            unions: Vec::new(),
14137            order_by: Vec::new(),
14138            limit: None,
14139            offset: None,
14140            limit_with_ties: false,
14141            window_check_exprs,
14142        };
14143        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
14144        // first set is the primary (already on stmt.group_by); each
14145        // further set becomes a UNION ALL peer with its dropped
14146        // keys (universe minus the set) replaced by NULL literals
14147        // in the peer's items and group_by. PG-legal: non-grouped
14148        // select items must be group keys or aggregates, so a
14149        // dropped key's occurrences in the projection are exactly
14150        // the ones to nullify.
14151        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14152        // over a plain GROUP BY (every argument must be a group key; the
14153        // mask is then 0) and rejects anything else with 42803. SPG's
14154        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14155        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14156        // function `grouping`".
14157        if grouping_sets.len() <= 1 {
14158            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14159            let mut calls: Vec<Expr> = Vec::new();
14160            for item in &stmt.items {
14161                if let SelectItem::Expr { expr, .. } = item {
14162                    Self::collect_grouping_calls(expr, &mut calls);
14163                }
14164            }
14165            if let Some(h) = &stmt.having {
14166                Self::collect_grouping_calls(h, &mut calls);
14167            }
14168            for call in &calls {
14169                let Expr::FunctionCall { args, .. } = call else {
14170                    continue;
14171                };
14172                for a in args {
14173                    if !keys.iter().any(|k| k == a) {
14174                        return Err(self.err(
14175                            "arguments to GROUPING must be grouping expressions of the associated query level"
14176                                .to_string(),
14177                        ));
14178                    }
14179                }
14180            }
14181            if !calls.is_empty() {
14182                for item in &mut stmt.items {
14183                    if let SelectItem::Expr { expr, .. } = item {
14184                        Self::substitute_grouping_calls(expr, &[]);
14185                    }
14186                }
14187                if let Some(h) = &mut stmt.having {
14188                    Self::substitute_grouping_calls(h, &[]);
14189                }
14190            }
14191        }
14192        if grouping_sets.len() > 1 {
14193            // The primary set's own dropped keys nullify in the
14194            // HEAD's projection too (GROUPING SETS's first set may
14195            // omit keys other sets use).
14196            let primary = grouping_sets[0].clone();
14197            let head_dropped: Vec<Expr> = grouping_universe
14198                .iter()
14199                .filter(|u| !primary.iter().any(|k| k == *u))
14200                .cloned()
14201                .collect();
14202            for set in grouping_sets.iter().skip(1) {
14203                let mut peer = stmt.clone();
14204                peer.unions = Vec::new();
14205                let dropped: Vec<&Expr> = grouping_universe
14206                    .iter()
14207                    .filter(|u| !set.iter().any(|k| k == *u))
14208                    .collect();
14209                // Empty set = grand-total group: `Some(vec![])` forces
14210                // the aggregate path (one collapsed row) instead of a
14211                // per-row passthrough. See the primary-set note above.
14212                peer.group_by = Some(set.clone());
14213                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14214                for item in &mut peer.items {
14215                    if let SelectItem::Expr { expr, alias } = item {
14216                        if dropped.iter().any(|d| *d == expr) {
14217                            // v7.39 — keep the dropped key's name on the
14218                            // NULL literal so the UNION output column
14219                            // (and any top-level ORDER BY on it) still
14220                            // resolves.
14221                            if alias.is_none()
14222                                && let Expr::Column(c) = &expr
14223                            {
14224                                *alias = Some(c.name.clone());
14225                            }
14226                            *expr = Expr::Literal(Literal::Null);
14227                        } else {
14228                            Self::substitute_grouping_calls(expr, &dropped_owned);
14229                        }
14230                    }
14231                }
14232                if let Some(h) = &mut peer.having {
14233                    Self::substitute_grouping_calls(h, &dropped_owned);
14234                }
14235                stmt.unions.push((UnionKind::All, peer));
14236            }
14237            for item in &mut stmt.items {
14238                if let SelectItem::Expr { expr, alias } = item {
14239                    if head_dropped.iter().any(|d| d == expr) {
14240                        if alias.is_none()
14241                            && let Expr::Column(c) = &expr
14242                        {
14243                            *alias = Some(c.name.clone());
14244                        }
14245                        *expr = Expr::Literal(Literal::Null);
14246                    } else {
14247                        Self::substitute_grouping_calls(expr, &head_dropped);
14248                    }
14249                }
14250            }
14251            if let Some(h) = &mut stmt.having {
14252                Self::substitute_grouping_calls(h, &head_dropped);
14253            }
14254            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14255            // (while `grouping_universe` / the per-branch sets are in scope). For
14256            // each grouping() call in it, inject a per-branch hidden column
14257            // `__grp_ord_K` carrying that branch's mask into the head + every
14258            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14259            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14260            // from the final output. A standalone grouping-set query has ORDER BY
14261            // (not an explicit set-op) next, so consuming it here is safe.
14262            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14263            // rollup carries the hierarchical order: sort by the grouping
14264            // keys with the rolled-up NULLs last, which is exactly the
14265            // interleaving both oracles emit. A client's own ORDER BY wins,
14266            // which is what MySQL does (MariaDB refuses to let one be
14267            // written at all).
14268            // The synthesised keys have to travel the SAME path a written
14269            // ORDER BY does: the block below is what turns a `grouping()`
14270            // call into the per-branch `__grp_ord_K` column the engine can
14271            // actually sort on. Bypassing it left a bare `grouping(text)`
14272            // for the evaluator to reject.
14273            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14274                self.parse_order_by_keys()?
14275            } else if mysql_rollup {
14276                Self::mysql_rollup_order(&grouping_universe)
14277            } else {
14278                Vec::new()
14279            };
14280            if !synthesised_or_parsed.is_empty() {
14281                let mut order_keys = synthesised_or_parsed;
14282                let mut grp_exprs: Vec<Expr> = Vec::new();
14283                for ob in &order_keys {
14284                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14285                }
14286                for (k, gexpr) in grp_exprs.iter().enumerate() {
14287                    let colname = alloc::format!("__grp_ord_{k}");
14288                    // Head branch (primary set) uses `head_dropped`.
14289                    let mut he = gexpr.clone();
14290                    Self::substitute_grouping_calls(&mut he, &head_dropped);
14291                    stmt.items.push(SelectItem::Expr {
14292                        expr: he,
14293                        alias: Some(colname.clone()),
14294                    });
14295                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14296                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14297                        let set = &grouping_sets[i + 1];
14298                        let dropped: Vec<Expr> = grouping_universe
14299                            .iter()
14300                            .filter(|u| !set.iter().any(|k| k == *u))
14301                            .cloned()
14302                            .collect();
14303                        let mut pe = gexpr.clone();
14304                        Self::substitute_grouping_calls(&mut pe, &dropped);
14305                        peer.items.push(SelectItem::Expr {
14306                            expr: pe,
14307                            alias: Some(colname.clone()),
14308                        });
14309                    }
14310                }
14311                for ob in &mut order_keys {
14312                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14313                }
14314                stmt.order_by = order_keys;
14315            }
14316        }
14317        Ok(stmt)
14318    }
14319
14320    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14321    /// as ORDER BY keys.
14322    ///
14323    /// Per key: the rollup marker, then the key. Sorting on the key alone
14324    /// is not enough, and a table with a NULL in it says why — MariaDB puts
14325    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14326    /// the ROLLUP-introduced NULL last, and both print as NULL.
14327    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14328    /// real group including the data-NULL one, 1 only for the row the
14329    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14330    /// rolls up to NULL|2, a|1, b|3, NULL|6.
14331    ///
14332    /// `#[inline(never)]`: its locals must not join the statement parser's
14333    /// frame, which round 430 measured sitting against the nesting guard.
14334    #[inline(never)]
14335    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14336        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14337        for e in keys {
14338            out.push(OrderBy {
14339                expr: Expr::FunctionCall {
14340                    name: "grouping".into(),
14341                    args: alloc::vec![e.clone()],
14342                },
14343                desc: false,
14344                nulls_first: None,
14345                collation: None,
14346            });
14347            out.push(OrderBy {
14348                expr: e.clone(),
14349                desc: false,
14350                // MySQL orders NULL first on an ascending key.
14351                nulls_first: Some(true),
14352                collation: None,
14353            });
14354        }
14355        out
14356    }
14357
14358    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14359    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14360    #[inline(never)]
14361    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14362        use crate::ast::MaintainKind;
14363        self.skip_paren_option_list();
14364        let kind = match self.peek() {
14365            // `TABLE` and `INDEX` lex as keywords, not identifiers.
14366            Token::Table | Token::Index => {
14367                self.advance();
14368                MaintainKind::ReindexRelation
14369            }
14370            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14371                "index" | "table" => {
14372                    self.advance();
14373                    MaintainKind::ReindexRelation
14374                }
14375                "schema" => {
14376                    self.advance();
14377                    MaintainKind::ReindexSchema
14378                }
14379                "system" | "database" => {
14380                    self.advance();
14381                    MaintainKind::Whole
14382                }
14383                // PG requires the object type; anything else is the
14384                // caller's problem, not something to swallow.
14385                _ => MaintainKind::ReindexRelation,
14386            },
14387            _ => MaintainKind::Whole,
14388        };
14389        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14390        // allows the plain form, so the modifier is recorded rather than
14391        // skipped. It still has no effect on how the reindex runs.
14392        let mut concurrently = false;
14393        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14394            self.advance();
14395            concurrently = true;
14396        }
14397        let target = self.take_optional_maintain_name();
14398        self.consume_until_statement_boundary();
14399        Ok(Statement::Maintain {
14400            kind,
14401            concurrently,
14402            target,
14403        })
14404    }
14405
14406    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14407    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14408    #[inline(never)]
14409    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14410        use crate::ast::MaintainKind;
14411        self.skip_paren_option_list();
14412        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14413            self.advance();
14414        }
14415        let target = self.take_optional_maintain_name();
14416        self.consume_until_statement_boundary();
14417        Ok(Statement::Maintain {
14418            kind: if target.is_some() {
14419                MaintainKind::ClusterRelation
14420            } else {
14421                MaintainKind::Whole
14422            },
14423            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14424            // transaction block quite happily (measured).
14425            concurrently: false,
14426            target,
14427        })
14428    }
14429
14430    /// The next token as a relation / schema name, when there is one.
14431    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14432        match self.peek() {
14433            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14434                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14435                _ => None,
14436            },
14437            _ => None,
14438        }
14439    }
14440
14441    /// A parenthesised option list, absorbed.
14442    fn skip_paren_option_list(&mut self) {
14443        if !matches!(self.peek(), Token::LParen) {
14444            return;
14445        }
14446        let mut depth = 0usize;
14447        loop {
14448            match self.advance() {
14449                Token::LParen => depth += 1,
14450                Token::RParen => {
14451                    depth -= 1;
14452                    if depth == 0 {
14453                        return;
14454                    }
14455                }
14456                Token::Eof => return,
14457                _ => {}
14458            }
14459        }
14460    }
14461
14462    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14463    /// column list.
14464    ///
14465    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14466    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14467    /// / ALL. The three that describe physical storage have no meaning
14468    /// here, so they parse and change nothing rather than making a
14469    /// dump that mentions them fail to load.
14470    ///
14471    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14472    /// parse chain the nesting sentinel is tuned against.
14473    #[inline(never)]
14474    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14475        self.advance(); // LIKE
14476        let source = self.expect_ident_like()?;
14477        let mut options = crate::ast::LikeOptions::default();
14478        loop {
14479            let including = match self.peek() {
14480                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14481                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14482                _ => break,
14483            };
14484            self.advance();
14485            // `ALL` lexes as its own keyword, not an identifier.
14486            let opt = if matches!(self.peek(), Token::All) {
14487                self.advance();
14488                alloc::string::String::from("all")
14489            } else {
14490                self.expect_ident_like()?
14491            };
14492            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14493                o.defaults = on;
14494                o.constraints = on;
14495                o.identity = on;
14496                o.generated = on;
14497                o.indexes = on;
14498                o.comments = on;
14499            };
14500            match opt.to_ascii_lowercase().as_str() {
14501                "all" => set(&mut options, including),
14502                "defaults" => options.defaults = including,
14503                "constraints" => options.constraints = including,
14504                "identity" => options.identity = including,
14505                "generated" => options.generated = including,
14506                "indexes" => options.indexes = including,
14507                "comments" => options.comments = including,
14508                // No storage model to copy into.
14509                "storage" | "statistics" | "compression" => {}
14510                other => {
14511                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14512                }
14513            }
14514        }
14515        Ok(crate::ast::LikeSpec {
14516            source,
14517            at,
14518            options,
14519        })
14520    }
14521
14522    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14523        // Caller already consumed CREATE; we're sitting on TABLE.
14524        debug_assert!(matches!(self.peek(), Token::Table));
14525        self.advance();
14526        let if_not_exists = self.consume_if_not_exists();
14527        let name = self.expect_ident_like()?;
14528        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14529        // child shape has no column list; the child inherits its
14530        // columns from the parent at engine-DDL time. Detect it
14531        // before the `(` requirement below.
14532        if matches!(self.peek(), Token::Partition)
14533            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14534        {
14535            self.advance(); // PARTITION
14536            self.advance(); // of
14537            let partition_of = self.parse_partition_of_tail()?;
14538            return Ok(Statement::CreateTable(CreateTableStatement {
14539                temporary: false,
14540                name,
14541                engine: None,
14542                columns: Vec::new(),
14543                like_specs: Vec::new(),
14544                inherits: Vec::new(),
14545                if_not_exists,
14546                foreign_keys: Vec::new(),
14547                table_constraints: Vec::new(),
14548                partition_by: None,
14549                partition_of: Some(partition_of),
14550            }));
14551        }
14552        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14553        // the materialized-view materialisation path (run the SELECT, infer the
14554        // column types, create + populate the table) but marks the node so the
14555        // executor creates a plain table without a mat-view registry entry.
14556        if matches!(self.peek(), Token::As) {
14557            self.advance();
14558            let body_stmt = self.parse_select_stmt()?;
14559            let Statement::Select(body) = body_stmt else {
14560                return Err(self.err(format!(
14561                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14562                )));
14563            };
14564            let with_data = self.parse_optional_with_data(true)?;
14565            return Ok(Statement::CreateMaterializedView(
14566                crate::ast::CreateMaterializedViewStatement {
14567                    temporary: false,
14568                    name,
14569                    if_not_exists,
14570                    columns: Vec::new(),
14571                    body,
14572                    with_data,
14573                    as_plain_table: true,
14574                },
14575            ));
14576        }
14577        if !matches!(self.peek(), Token::LParen) {
14578            return Err(self.err(format!(
14579                "expected '(' after table name, got {:?}",
14580                self.peek()
14581            )));
14582        }
14583        self.advance();
14584        let mut columns = Vec::new();
14585        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14586        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14587        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14588        loop {
14589            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14590            // column list. It is how a child that adds nothing of its own is
14591            // written, and this loop demanded at least one entry: `syntax
14592            // error at or near ")"`. The child takes the parent's columns,
14593            // which the INHERITS clause already arranges.
14594            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14595                self.advance();
14596                break;
14597            }
14598            // v7.6.0 / v7.9.18 — distinguish table-level constraint
14599            // clauses from column definitions. Constraints start
14600            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14601            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14602            // a column.
14603            if self.peek_table_level_pk_start() {
14604                table_constraints.push(self.parse_table_level_primary_key()?);
14605            } else if matches!(self.peek(), Token::Like) {
14606                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14607                // <opt> ]*`. The source table's shape lives in the catalog,
14608                // so this records the clause and the engine expands it.
14609                like_specs.push(self.parse_create_table_like(columns.len())?);
14610            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14611                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14612                table_constraints.push(self.parse_table_level_exclude()?);
14613            } else if self.peek_table_level_unique_start() {
14614                table_constraints.push(self.parse_table_level_unique()?);
14615            } else if self.peek_table_level_check_start() {
14616                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14617                table_constraints.push(self.parse_table_level_check()?);
14618            } else if self.peek_mysql_inline_key_start() {
14619                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14620                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14621                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14622                // inside the column list. Skip name + paren list;
14623                // for UNIQUE KEY, register as a UC.
14624                if let Some(uc) = self.parse_mysql_inline_key()? {
14625                    table_constraints.push(uc);
14626                }
14627            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14628                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14629                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14630                // CHECK is named, and the named-CONSTRAINT arm used
14631                // to accept FOREIGN KEY only. The name is accepted
14632                // and discarded — same handling as every other SPG
14633                // constraint name.
14634                self.advance(); // CONSTRAINT
14635                // v7.39 (read01 round 48) — the name is kept now: the schema
14636                // stores it, so DROP / RENAME CONSTRAINT can find it.
14637                let con_name = self.expect_ident_like()?;
14638                let mut tc = match kind {
14639                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14640                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14641                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14642                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14643                };
14644                match &mut tc {
14645                    crate::ast::TableConstraint::Check { name, .. }
14646                    | crate::ast::TableConstraint::Unique { name, .. }
14647                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
14648                    | crate::ast::TableConstraint::Exclude { name, .. } => {
14649                        *name = Some(con_name);
14650                    }
14651                    _ => {}
14652                }
14653                table_constraints.push(tc);
14654            } else if self.peek_constraint_or_fk_start() {
14655                foreign_keys.push(self.parse_table_level_fk()?);
14656            } else {
14657                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14658                // v7.13.0 — fold inline UNIQUE / CHECK column
14659                // constraints into table-level entries so the
14660                // engine path stays uniform.
14661                if col.is_unique {
14662                    table_constraints.push(crate::ast::TableConstraint::Unique {
14663                        name: None,
14664                        columns: alloc::vec![col.name.clone()],
14665                        nulls_not_distinct: col.unique_nulls_not_distinct,
14666                        deferrable: col.constraint_deferrable,
14667                        initially_deferred: col.constraint_initially_deferred,
14668                    });
14669                }
14670                if let Some(check_expr) = col.check.clone() {
14671                    table_constraints.push(crate::ast::TableConstraint::Check {
14672                        name: None,
14673                        expr: check_expr,
14674                        not_valid: false,
14675                    });
14676                }
14677                columns.push(col);
14678                if let Some(fk) = col_level_fk {
14679                    foreign_keys.push(fk);
14680                }
14681            }
14682            match self.peek() {
14683                Token::Comma => {
14684                    self.advance();
14685                }
14686                Token::RParen => {
14687                    self.advance();
14688                    break;
14689                }
14690                other => {
14691                    return Err(
14692                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14693                    );
14694                }
14695            }
14696        }
14697        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14698        // `CREATE TABLE k (LIKE t)` is a complete definition even though
14699        // nothing is written between the parentheses.
14700        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14701        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14702        // empty parentheses were a parse error in their own right — quite apart
14703        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14704        // SPG does not have (filed separately).
14705        let _ = &like_specs;
14706        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14707        // It sits between the column list and the MySQL table options,
14708        // and it was a syntax error until this round.
14709        let mut inherits: Vec<String> = Vec::new();
14710        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14711            if k.eq_ignore_ascii_case("inherits"))
14712        {
14713            self.advance();
14714            if !matches!(self.peek(), Token::LParen) {
14715                return Err(self.err(alloc::format!(
14716                    "expected ( after INHERITS, got {:?}",
14717                    self.peek()
14718                )));
14719            }
14720            self.advance();
14721            loop {
14722                inherits.push(self.expect_ident_like()?);
14723                if matches!(self.peek(), Token::Comma) {
14724                    self.advance();
14725                    continue;
14726                }
14727                break;
14728            }
14729            if !matches!(self.peek(), Token::RParen) {
14730                return Err(self.err(alloc::format!(
14731                    "expected ) closing INHERITS, got {:?}",
14732                    self.peek()
14733                )));
14734            }
14735            self.advance();
14736        }
14737        // v7.14.0 — consume MySQL/MariaDB table options after the
14738        // closing `)`. mysqldump emits things like
14739        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14740        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14741        // SPG accepts all forms as no-ops (each option is
14742        // `<ident> [=] <ident-or-string>` separated by whitespace).
14743        let engine = self.consume_mysql_table_options();
14744        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14745        // SPG has no per-table reloptions, so accept and ignore them so a
14746        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14747        self.consume_with_reloptions();
14748        // v7.37.6-B — declarative-partition-parent suffix
14749        // (`PARTITION BY RANGE (key_col)`) sits after the column
14750        // list + MySQL table-options. v7.37.6-B only accepts RANGE
14751        // and locks the key column at one ident; the engine then
14752        // verifies the column type is TIMESTAMPTZ.
14753        let partition_by = if matches!(self.peek(), Token::Partition) {
14754            self.advance(); // PARTITION
14755            if !self.peek_is_by() {
14756                return Err(self.err(format!(
14757                    "expected BY after PARTITION, got {:?}",
14758                    self.peek()
14759                )));
14760            }
14761            self.advance();
14762            Some(self.parse_partition_by_tail()?)
14763        } else {
14764            None
14765        };
14766        Ok(Statement::CreateTable(CreateTableStatement {
14767            temporary: false,
14768            name,
14769            engine,
14770            columns,
14771            like_specs,
14772            inherits,
14773            if_not_exists,
14774            foreign_keys,
14775            table_constraints,
14776            partition_by,
14777            partition_of: None,
14778        }))
14779    }
14780
14781    /// v7.37.6-B — case-insensitive ident match helper for the
14782    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14783    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14784    /// didn't burn a global keyword slot for each (see the
14785    /// `Token::Partition` doc-comment in `lexer.rs`).
14786    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14787        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14788    }
14789
14790    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14791    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14792    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14793        use crate::ast::{PartitionBySpec, PartitionKindAst};
14794        let kind = match self.peek() {
14795            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14796                self.advance();
14797                PartitionKindAst::Range
14798            }
14799            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14800                self.advance();
14801                PartitionKindAst::List
14802            }
14803            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14804                self.advance();
14805                PartitionKindAst::Hash
14806            }
14807            other => {
14808                return Err(self.err(format!(
14809                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14810                )));
14811            }
14812        };
14813        if !matches!(self.peek(), Token::LParen) {
14814            return Err(self.err(format!(
14815                "expected '(' after PARTITION BY <strategy>, got {:?}",
14816                self.peek()
14817            )));
14818        }
14819        self.advance();
14820        let mut key_columns = Vec::new();
14821        loop {
14822            key_columns.push(self.expect_ident_like()?);
14823            match self.peek() {
14824                Token::Comma => {
14825                    self.advance();
14826                }
14827                Token::RParen => {
14828                    self.advance();
14829                    break;
14830                }
14831                other => {
14832                    return Err(self.err(format!(
14833                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14834                    )));
14835                }
14836            }
14837        }
14838        if key_columns.is_empty() {
14839            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14840        }
14841        Ok(PartitionBySpec { kind, key_columns })
14842    }
14843
14844    /// v7.37.6-B — after `PARTITION OF`, expect
14845    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14846    /// or
14847    ///   <parent> DEFAULT
14848    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14849        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14850        let parent_name = self.expect_ident_like()?;
14851        // v7.37.6-B rejects an explicit column list — the child
14852        // inherits from the parent. mailrs round-7 taught us that
14853        // CREATE TABLE-side schema reconciliation hides drift, so
14854        // we surface this as a parse error rather than silently
14855        // ignoring user columns.
14856        if matches!(self.peek(), Token::LParen) {
14857            return Err(self.err(
14858                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14859                 at v7.37.6-B; the child inherits its columns from the parent"
14860                    .to_string(),
14861            ));
14862        }
14863        let bounds = match self.peek() {
14864            Token::Default => {
14865                self.advance();
14866                PartitionOfBoundsAst::Default
14867            }
14868            Token::For => {
14869                self.advance();
14870                if !matches!(self.peek(), Token::Values) {
14871                    return Err(
14872                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14873                    );
14874                }
14875                self.advance();
14876                // WITH is not a reserved Token in the lexer — it lexes
14877                // as Token::Ident("with"). Disambiguate manually.
14878                let want_with = matches!(
14879                    self.peek(),
14880                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14881                );
14882                if want_with {
14883                    self.advance();
14884                    if !matches!(self.peek(), Token::LParen) {
14885                        return Err(self.err(format!(
14886                            "expected '(' after FOR VALUES WITH, got {:?}",
14887                            self.peek()
14888                        )));
14889                    }
14890                    self.advance();
14891                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14892                    loop {
14893                        let key = self.expect_ident_like()?;
14894                        let n = match self.peek().clone() {
14895                            Token::Integer(v) if u32::try_from(v).is_ok() => {
14896                                self.advance();
14897                                v as u32
14898                            }
14899                            other => {
14900                                return Err(self.err(format!(
14901                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14902                                )));
14903                            }
14904                        };
14905                        match key.to_ascii_uppercase().as_str() {
14906                            "MODULUS" => modulus = Some(n),
14907                            "REMAINDER" => remainder = Some(n),
14908                            other => {
14909                                return Err(self.err(format!(
14910                                    "FOR VALUES WITH: unknown key {other:?}; \
14911                                     expected MODULUS or REMAINDER"
14912                                )));
14913                            }
14914                        }
14915                        match self.peek() {
14916                            Token::Comma => {
14917                                self.advance();
14918                            }
14919                            Token::RParen => {
14920                                self.advance();
14921                                break;
14922                            }
14923                            other => {
14924                                return Err(self.err(format!(
14925                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14926                                )));
14927                            }
14928                        }
14929                    }
14930                    let modulus = modulus
14931                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14932                    let remainder = remainder.ok_or_else(|| {
14933                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14934                    })?;
14935                    if modulus == 0 {
14936                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14937                    }
14938                    if remainder >= modulus {
14939                        return Err(self.err(format!(
14940                            "FOR VALUES WITH: REMAINDER ({remainder}) \
14941                             must be < MODULUS ({modulus})"
14942                        )));
14943                    }
14944                    PartitionOfBoundsAst::Hash { modulus, remainder }
14945                } else {
14946                    match self.peek() {
14947                        Token::From => {
14948                            self.advance();
14949                            let lower = Box::new(self.parse_partition_bound_expr()?);
14950                            if !matches!(self.peek(), Token::To) {
14951                                return Err(self.err(format!(
14952                                    "expected TO after FROM (...), got {:?}",
14953                                    self.peek()
14954                                )));
14955                            }
14956                            self.advance();
14957                            let upper = Box::new(self.parse_partition_bound_expr()?);
14958                            PartitionOfBoundsAst::Range { lower, upper }
14959                        }
14960                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14961                        Token::In => {
14962                            self.advance();
14963                            if !matches!(self.peek(), Token::LParen) {
14964                                return Err(self.err(format!(
14965                                    "expected '(' after FOR VALUES IN, got {:?}",
14966                                    self.peek()
14967                                )));
14968                            }
14969                            self.advance();
14970                            let mut values = Vec::new();
14971                            loop {
14972                                values.push(self.parse_expr(0)?);
14973                                match self.peek() {
14974                                    Token::Comma => {
14975                                        self.advance();
14976                                    }
14977                                    Token::RParen => {
14978                                        self.advance();
14979                                        break;
14980                                    }
14981                                    other => {
14982                                        return Err(self.err(format!(
14983                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14984                                    )));
14985                                    }
14986                                }
14987                            }
14988                            if values.is_empty() {
14989                                return Err(self.err(
14990                                    "FOR VALUES IN requires at least one literal".to_string(),
14991                                ));
14992                            }
14993                            PartitionOfBoundsAst::List { values }
14994                        }
14995                        other => {
14996                            return Err(self.err(format!(
14997                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14998                            )));
14999                        }
15000                    }
15001                }
15002            }
15003            other => {
15004                return Err(self.err(format!(
15005                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
15006                )));
15007            }
15008        };
15009        Ok(PartitionOfSpec {
15010            parent_name,
15011            bounds,
15012        })
15013    }
15014
15015    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
15016    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
15017    /// markers (no-arg builtins) so the engine resolves them
15018    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
15019    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
15020        if !matches!(self.peek(), Token::LParen) {
15021            return Err(self.err(format!(
15022                "expected '(' before partition bound, got {:?}",
15023                self.peek()
15024            )));
15025        }
15026        self.advance();
15027        let expr = match self.peek() {
15028            Token::Ident(s) | Token::QuotedIdent(s)
15029                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
15030            {
15031                let name = s.to_ascii_uppercase();
15032                self.advance();
15033                crate::ast::Expr::FunctionCall {
15034                    name,
15035                    args: Vec::new(),
15036                }
15037            }
15038            _ => self.parse_expr(0)?,
15039        };
15040        if !matches!(self.peek(), Token::RParen) {
15041            return Err(self.err(format!(
15042                "expected ')' after partition bound, got {:?}",
15043                self.peek()
15044            )));
15045        }
15046        self.advance();
15047        Ok(expr)
15048    }
15049
15050    /// v7.14.0 — true when the next tokens look like an inline
15051    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
15052    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
15053    /// — each followed by an optional name + `(...)`. Critical:
15054    /// a column NAMED `key` / `index` (PG accepts as ident) must
15055    /// NOT be mistaken for the KEY constraint shape. We disambig
15056    /// by requiring the keyword to be followed by either `(` or
15057    /// `<ident> (`.
15058    fn peek_mysql_inline_key_start(&self) -> bool {
15059        let cur = self.peek();
15060        // Shapes:
15061        //   KEY (cols)
15062        //   KEY name (cols)
15063        //   INDEX (cols)
15064        //   INDEX name (cols)
15065        //   UNIQUE KEY [name] (cols)
15066        //   UNIQUE INDEX [name] (cols)
15067        //   FULLTEXT [KEY|INDEX] [name] (cols)
15068        //   SPATIAL [KEY|INDEX] [name] (cols)
15069        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
15070            // tokens at skip = the position AFTER the index-form
15071            // keywords (KEY/INDEX) have been consumed.
15072            match self.tokens.get(skip) {
15073                Some(Token::LParen) => true,
15074                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
15075                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
15076                }
15077                _ => false,
15078            }
15079        };
15080        // `INDEX` lexes as Token::Index (reserved), not as
15081        // Token::Ident("index"). Both shapes count as a KEY/INDEX
15082        // start; the peek helper below handles either.
15083        let is_key_or_index_tok = |t: &Token| -> bool {
15084            matches!(t, Token::Index)
15085                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
15086        };
15087        match cur {
15088            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
15089            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15090                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
15091            }
15092            Token::Ident(s)
15093                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
15094            {
15095                let nxt = self.tokens.get(self.pos + 1);
15096                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
15097                    self.pos + 2
15098                } else {
15099                    self.pos + 1
15100                };
15101                after_keyword_followed_by_paren_or_ident_paren(after_after)
15102            }
15103            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
15104                let nxt = self.tokens.get(self.pos + 1);
15105                if !nxt.is_some_and(is_key_or_index_tok) {
15106                    return false;
15107                }
15108                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
15109            }
15110            _ => false,
15111        }
15112    }
15113
15114    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
15115    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
15116    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
15117    /// returns Some(TableConstraint::Index) so the engine builds
15118    /// a real BTree index on the leading column (mysqldump
15119    /// `KEY idx_posts_author (author_id)` shape).
15120    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
15121    /// (the storage layer has no matching AM).
15122    fn parse_mysql_inline_key(
15123        &mut self,
15124    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
15125        // Detect UNIQUE prefix.
15126        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
15127        {
15128            self.advance();
15129            true
15130        } else {
15131            false
15132        };
15133        // Consume FULLTEXT / SPATIAL prefix and record which one
15134        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
15135        // dedicated TableConstraint variant so the engine can
15136        // build a tsvector-GIN; SPATIAL still has no matching
15137        // AM, so it falls back to accept-as-no-op.
15138        let mut is_fulltext = false;
15139        let mut is_spatial = false;
15140        if let Token::Ident(s) = self.peek().clone() {
15141            if s.eq_ignore_ascii_case("fulltext") {
15142                self.advance();
15143                is_fulltext = true;
15144            } else if s.eq_ignore_ascii_case("spatial") {
15145                self.advance();
15146                is_spatial = true;
15147            }
15148        }
15149        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
15150        // (reserved); accept either token shape.
15151        match self.peek() {
15152            Token::Index => {
15153                self.advance();
15154            }
15155            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15156                self.advance();
15157            }
15158            other => {
15159                return Err(self.err(alloc::format!(
15160                    "expected KEY/INDEX in inline index declaration, got {other:?}"
15161                )));
15162            }
15163        }
15164        // Optional index name (an ident before the `(`).
15165        // v7.15.0 — capture the name when present so the engine
15166        // builds the secondary index under the user's chosen
15167        // name (matches mysqldump's `KEY idx_x (col)` shape).
15168        let mut idx_name: Option<String> = None;
15169        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15170            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15171        {
15172            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15173                idx_name = Some(s);
15174            }
15175        }
15176        // Optional `USING BTREE` / `USING HASH` (MySQL).
15177        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15178            self.advance();
15179            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15180                self.advance();
15181            }
15182        }
15183        // Required column list `(col [, col]*)`.
15184        if !matches!(self.peek(), Token::LParen) {
15185            return Err(self.err(alloc::format!(
15186                "expected '(' in inline KEY/INDEX, got {:?}",
15187                self.peek()
15188            )));
15189        }
15190        self.advance();
15191        let mut cols: Vec<String> = Vec::new();
15192        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15193            self.advance();
15194            cols.push(s);
15195            // Skip optional `(length)` per-column prefix.
15196            if matches!(self.peek(), Token::LParen) {
15197                let mut depth = 1usize;
15198                self.advance();
15199                while depth > 0 {
15200                    match self.peek() {
15201                        Token::LParen => depth += 1,
15202                        Token::RParen => depth -= 1,
15203                        Token::Eof => break,
15204                        _ => {}
15205                    }
15206                    self.advance();
15207                }
15208            }
15209            // Skip optional ASC / DESC.
15210            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15211                || matches!(self.peek(), Token::Asc | Token::Desc)
15212            {
15213                self.advance();
15214            }
15215            if matches!(self.peek(), Token::Comma) {
15216                self.advance();
15217                continue;
15218            }
15219            break;
15220        }
15221        if matches!(self.peek(), Token::RParen) {
15222            self.advance();
15223        }
15224        // Trailing options on the inline index — comment / etc.
15225        // Skip until comma or `)`.
15226        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15227            self.advance();
15228        }
15229        if cols.is_empty() {
15230            return Ok(None);
15231        }
15232        if is_unique {
15233            // Carry the captured idx_name on UNIQUE too so future
15234            // engine work can name the underlying BTree
15235            // accordingly; today the unique-constraint installer
15236            // synthesises the name itself, but Display round-trip
15237            // benefits from preserving it.
15238            Ok(Some(crate::ast::TableConstraint::Unique {
15239                name: idx_name,
15240                columns: cols,
15241                nulls_not_distinct: false,
15242                // MySQL inline UNIQUE KEY has no deferral vocabulary.
15243                deferrable: false,
15244                initially_deferred: false,
15245            }))
15246        } else if is_fulltext {
15247            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15248            // routes through `TableConstraint::FulltextIndex`;
15249            // the engine builds a tsvector-GIN over each named
15250            // column so MATCH AGAINST gets a real inverted
15251            // index instead of a silently-dropped declaration.
15252            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15253                name: idx_name,
15254                columns: cols,
15255            }))
15256        } else if is_spatial {
15257            // SPG has no native SPATIAL AM. Accept-as-no-op
15258            // (declaration is parsed, but no index is built).
15259            Ok(None)
15260        } else {
15261            // v7.15.0 — plain KEY / INDEX builds a real BTree
15262            // secondary index.
15263            Ok(Some(crate::ast::TableConstraint::Index {
15264                name: idx_name,
15265                columns: cols,
15266            }))
15267        }
15268    }
15269
15270    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15271    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15272    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15273    /// (in any order, separated by whitespace).
15274    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15275    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15276    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15277    /// bare ident here, and only the parenthesised form is reloptions (so this
15278    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15279    fn consume_with_reloptions(&mut self) {
15280        let is_with = matches!(
15281            self.peek(),
15282            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15283        );
15284        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15285            return;
15286        }
15287        self.advance(); // WITH
15288        self.advance(); // (
15289        let mut depth = 1u32;
15290        while depth > 0 && !matches!(self.peek(), Token::Eof) {
15291            match self.peek() {
15292                Token::LParen => depth += 1,
15293                Token::RParen => depth -= 1,
15294                _ => {}
15295            }
15296            self.advance();
15297        }
15298    }
15299
15300    /// v7.39 — returns the `ENGINE=` name, which used to be consumed and
15301    /// dropped with everything else here. The rest of the MySQL table
15302    /// options genuinely have no meaning for SPG's storage; the engine
15303    /// name does, because MySQL REFUSES one it does not know and a dump
15304    /// with a typo in it should not quietly become a table.
15305    fn consume_mysql_table_options(&mut self) -> Option<alloc::string::String> {
15306        let mut engine: Option<alloc::string::String> = None;
15307        loop {
15308            // Heuristic: a table option is an ident (or `DEFAULT`
15309            // reserved keyword) followed by `=` and an
15310            // ident / string / integer.
15311            let name_lc = match self.peek().clone() {
15312                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15313                Token::Default => alloc::string::String::from("default"),
15314                _ => break,
15315            };
15316            let known = matches!(
15317                name_lc.as_str(),
15318                "engine"
15319                    | "default"
15320                    | "charset"
15321                    | "collate"
15322                    | "auto_increment"
15323                    | "row_format"
15324                    | "comment"
15325                    | "pack_keys"
15326                    | "stats_persistent"
15327                    | "stats_auto_recalc"
15328                    | "stats_sample_pages"
15329                    | "key_block_size"
15330                    | "tablespace"
15331                    | "min_rows"
15332                    | "max_rows"
15333                    | "checksum"
15334                    | "delay_key_write"
15335                    | "insert_method"
15336                    | "data"
15337                    | "index"
15338                    | "encryption"
15339                    | "compression"
15340            );
15341            if !known {
15342                break;
15343            }
15344            self.advance(); // option name
15345            // `DEFAULT` optional prefix is followed by `CHARSET` /
15346            // `COLLATE`; consume the next ident too.
15347            if name_lc == "default" {
15348                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15349                    self.advance();
15350                }
15351            }
15352            if matches!(self.peek(), Token::Eq) {
15353                self.advance();
15354            }
15355            match self.peek().clone() {
15356                Token::Ident(v) | Token::QuotedIdent(v) | Token::String(v) => {
15357                    if name_lc == "engine" {
15358                        // v7.39.3 — as WRITTEN. MySQL 9.7.2 refuses an
15359                        // engine it does not know and names it back
15360                        // exactly: `Unknown storage engine 'NoSuchEng'`,
15361                        // measured. The lexer folds a bare identifier, so
15362                        // the message quoted a name the dump did not
15363                        // contain, which is the one thing that message is
15364                        // for. Guarded the same way the column spelling
15365                        // is: the span runs to the next token, so what
15366                        // comes back has to be the same word.
15367                        let written = self
15368                            .source_span(self.pos, self.pos)
15369                            .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
15370                            .filter(|raw| raw.eq_ignore_ascii_case(&v))
15371                            .map(alloc::string::String::from);
15372                        engine = Some(written.unwrap_or(v));
15373                    }
15374                    self.advance();
15375                }
15376                Token::Integer(_) => {
15377                    self.advance();
15378                }
15379                _ => {}
15380            }
15381        }
15382        engine
15383    }
15384
15385    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15386    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15387    /// sure (otherwise a column literally named `primary` would
15388    /// be mistaken).
15389    fn peek_table_level_pk_start(&self) -> bool {
15390        let cur = self.peek();
15391        let nxt = self.tokens.get(self.pos + 1);
15392        let nxt2 = self.tokens.get(self.pos + 2);
15393        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15394        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15395        let is_lparen = matches!(nxt2, Some(Token::LParen));
15396        is_primary && is_key && is_lparen
15397    }
15398
15399    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15400    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15401    /// (mailrs round-5 G10).
15402    fn peek_table_level_unique_start(&self) -> bool {
15403        let cur = self.peek();
15404        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15405        if !is_unique {
15406            return false;
15407        }
15408        let n1 = self.tokens.get(self.pos + 1);
15409        // Plain `UNIQUE (…)`.
15410        if matches!(n1, Some(Token::LParen)) {
15411            return true;
15412        }
15413        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15414        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15415        if !is_nulls {
15416            return false;
15417        }
15418        let n2 = self.tokens.get(self.pos + 2);
15419        let n3 = self.tokens.get(self.pos + 3);
15420        let n4 = self.tokens.get(self.pos + 4);
15421        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15422        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15423            return true;
15424        }
15425        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15426        if matches!(n2, Some(Token::Not))
15427            && matches!(n3, Some(Token::Distinct))
15428            && matches!(n4, Some(Token::LParen))
15429        {
15430            return true;
15431        }
15432        false
15433    }
15434
15435    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15436        self.advance(); // PRIMARY
15437        self.advance(); // KEY
15438        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15439        // v7.39 (round 711) — the trailer's values are CARRIED now; round
15440        // 621 consumed and dropped them (the storing half of F08).
15441        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15442        Ok(crate::ast::TableConstraint::PrimaryKey {
15443            name: None,
15444            columns,
15445            deferrable,
15446            initially_deferred,
15447        })
15448    }
15449
15450    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15451        self.advance(); // UNIQUE
15452        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15453        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15454        // is `NULLS DISTINCT` per the SQL standard.
15455        let mut nulls_not_distinct = false;
15456        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15457            let n1 = self.tokens.get(self.pos + 1);
15458            let n2 = self.tokens.get(self.pos + 2);
15459            let is_not = matches!(n1, Some(Token::Not));
15460            let is_distinct = matches!(n2, Some(Token::Distinct));
15461            if is_not && is_distinct {
15462                self.advance(); // NULLS
15463                self.advance(); // NOT
15464                self.advance(); // DISTINCT
15465                nulls_not_distinct = true;
15466            } else if matches!(n1, Some(Token::Distinct)) {
15467                self.advance(); // NULLS
15468                self.advance(); // DISTINCT
15469            }
15470        }
15471        let columns = self.parse_paren_ident_list("UNIQUE")?;
15472        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15473        Ok(crate::ast::TableConstraint::Unique {
15474            name: None,
15475            columns,
15476            nulls_not_distinct,
15477            deferrable,
15478            initially_deferred,
15479        })
15480    }
15481
15482    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15483    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15484    /// expression.
15485    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15486    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15487    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15488    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15489    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15490    /// commit: `NOT` starts no other suffix here, but reading both
15491    /// tokens before advancing keeps the caller's error message intact
15492    /// if someone writes `NOT NULL` by mistake.
15493    fn parse_not_valid_suffix(&mut self) -> bool {
15494        if !matches!(self.peek(), Token::Not) {
15495            return false;
15496        }
15497        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15498        {
15499            return false;
15500        }
15501        self.advance();
15502        self.advance();
15503        true
15504    }
15505
15506    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15507        self.advance(); // EXCLUDE
15508        // Optional `USING <method>`.
15509        let mut method = None;
15510        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15511            self.advance();
15512            method = Some(match self.advance() {
15513                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15514                other => {
15515                    return Err(self.err(alloc::format!(
15516                        "expected index method after USING, got {other:?}"
15517                    )));
15518                }
15519            });
15520        }
15521        if !matches!(self.peek(), Token::LParen) {
15522            return Err(self.err(alloc::format!(
15523                "expected '(' after EXCLUDE, got {:?}",
15524                self.peek()
15525            )));
15526        }
15527        self.advance();
15528        let mut elements: Vec<(String, String)> = Vec::new();
15529        loop {
15530            let col = match self.advance() {
15531                Token::Ident(s) | Token::QuotedIdent(s) => s,
15532                other => {
15533                    return Err(self.err(alloc::format!(
15534                        "expected column name in EXCLUDE, got {other:?}"
15535                    )));
15536                }
15537            };
15538            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15539                return Err(self.err(alloc::format!(
15540                    "expected WITH after EXCLUDE column, got {:?}",
15541                    self.peek()
15542                )));
15543            }
15544            self.advance();
15545            let op = match self.advance() {
15546                Token::InetOverlap => String::from("&&"),
15547                Token::Intersects => String::from("?#"),
15548                Token::IsBelow => String::from("<^"),
15549                Token::IsAbove => String::from(">^"),
15550                Token::PatternLt => String::from("~<~"),
15551                Token::PatternLtEq => String::from("~<=~"),
15552                Token::PatternGt => String::from("~>~"),
15553                Token::PatternGtEq => String::from("~>=~"),
15554                Token::TsMatchOld => String::from("@@@"),
15555                Token::Eq => String::from("="),
15556                Token::JsonContains => String::from("@>"),
15557                Token::JsonContainedBy => String::from("<@"),
15558                Token::OverLeft => String::from("&<"),
15559                Token::OverRight => String::from("&>"),
15560                other => {
15561                    return Err(self.err(alloc::format!(
15562                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15563                    )));
15564                }
15565            };
15566            elements.push((col, op));
15567            if matches!(self.peek(), Token::Comma) {
15568                self.advance();
15569                continue;
15570            }
15571            break;
15572        }
15573        if !matches!(self.peek(), Token::RParen) {
15574            return Err(self.err(alloc::format!(
15575                "expected ')' to close EXCLUDE, got {:?}",
15576                self.peek()
15577            )));
15578        }
15579        self.advance();
15580        Ok(crate::ast::TableConstraint::Exclude {
15581            name: None,
15582            method,
15583            elements,
15584        })
15585    }
15586
15587    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15588        self.advance(); // CHECK
15589        if !matches!(self.peek(), Token::LParen) {
15590            return Err(self.err(alloc::format!(
15591                "expected '(' after CHECK, got {:?}",
15592                self.peek()
15593            )));
15594        }
15595        self.advance();
15596        let expr = self.parse_expr(0)?;
15597        if !matches!(self.peek(), Token::RParen) {
15598            return Err(self.err(alloc::format!(
15599                "expected ')' to close CHECK predicate, got {:?}",
15600                self.peek()
15601            )));
15602        }
15603        self.advance();
15604        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15605        // are no existing rows for PG to skip, so it rejects the suffix.
15606        Ok(crate::ast::TableConstraint::Check {
15607            name: None,
15608            expr,
15609            not_valid: false,
15610        })
15611    }
15612
15613    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15614    fn peek_table_level_check_start(&self) -> bool {
15615        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15616    }
15617
15618    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15619    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15620    /// on the dedicated FK path (`parse_table_level_fk` consumes its
15621    /// own CONSTRAINT prefix).
15622    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15623        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15624            return None;
15625        }
15626        // tokens[pos+1] is the constraint name (any ident-like);
15627        // tokens[pos+2] is the kind keyword.
15628        match self.tokens.get(self.pos + 2) {
15629            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15630                Some(NamedTableConstraintKind::Check)
15631            }
15632            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15633                Some(NamedTableConstraintKind::Unique)
15634            }
15635            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15636                Some(NamedTableConstraintKind::PrimaryKey)
15637            }
15638            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15639                Some(NamedTableConstraintKind::Exclude)
15640            }
15641            _ => None,
15642        }
15643    }
15644
15645    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15646        if !matches!(self.peek(), Token::LParen) {
15647            return Err(self.err(alloc::format!(
15648                "expected '(' after {ctx}, got {:?}",
15649                self.peek()
15650            )));
15651        }
15652        self.advance();
15653        let mut out = Vec::new();
15654        loop {
15655            out.push(self.expect_ident_like()?);
15656            match self.peek() {
15657                Token::Comma => {
15658                    self.advance();
15659                }
15660                Token::RParen => {
15661                    self.advance();
15662                    break;
15663                }
15664                other => {
15665                    return Err(self.err(alloc::format!(
15666                        "expected ',' or ')' in {ctx} list, got {other:?}"
15667                    )));
15668                }
15669            }
15670        }
15671        if out.is_empty() {
15672            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15673        }
15674        Ok(out)
15675    }
15676
15677    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15678    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15679    /// table-level FK; a column def never starts with either keyword
15680    /// (column names are not in this reserved set).
15681    fn peek_constraint_or_fk_start(&self) -> bool {
15682        let is_constraint_kw = matches!(
15683            self.peek(),
15684            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15685        );
15686        let is_foreign_kw = matches!(
15687            self.peek(),
15688            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15689        );
15690        is_constraint_kw || is_foreign_kw
15691    }
15692
15693    /// v7.6.0 — parse a table-level FK clause:
15694    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15695    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15696    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15697        let mut name: Option<String> = None;
15698        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15699            self.advance();
15700            name = Some(self.expect_ident_like()?);
15701        }
15702        // `FOREIGN`
15703        match self.advance() {
15704            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15705            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15706        }
15707        // `KEY`
15708        match self.advance() {
15709            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15710            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15711        }
15712        // `(col, col, ...)`
15713        if !matches!(self.peek(), Token::LParen) {
15714            return Err(self.err(format!(
15715                "expected '(' after FOREIGN KEY, got {:?}",
15716                self.peek()
15717            )));
15718        }
15719        self.advance();
15720        let mut columns = Vec::new();
15721        loop {
15722            columns.push(self.expect_ident_like()?);
15723            match self.peek() {
15724                Token::Comma => {
15725                    self.advance();
15726                }
15727                Token::RParen => {
15728                    self.advance();
15729                    break;
15730                }
15731                other => {
15732                    return Err(self.err(format!(
15733                        "expected ',' or ')' in FK column list, got {other:?}"
15734                    )));
15735                }
15736            }
15737        }
15738        if columns.is_empty() {
15739            return Err(self.err("FOREIGN KEY requires at least one column".into()));
15740        }
15741        let (
15742            parent_table,
15743            parent_columns,
15744            on_delete,
15745            on_update,
15746            match_type,
15747            deferrable,
15748            initially_deferred,
15749        ) = self.parse_references_tail(columns.len())?;
15750        Ok(ForeignKeyConstraint {
15751            name,
15752            columns,
15753            parent_table,
15754            parent_columns,
15755            on_delete,
15756            on_update,
15757            match_type,
15758            deferrable,
15759            initially_deferred,
15760        })
15761    }
15762
15763    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15764    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15765    /// the local column count, used to default the parent column
15766    /// list when omitted (SQL spec: parent's PK is implied).
15767    fn parse_references_tail(
15768        &mut self,
15769        expected_arity: usize,
15770    ) -> Result<
15771        (
15772            String,
15773            Vec<String>,
15774            FkAction,
15775            FkAction,
15776            crate::ast::MatchType,
15777            // v7.39 (round 288) — deferrable, initially_deferred.
15778            bool,
15779            bool,
15780        ),
15781        ParseError,
15782    > {
15783        match self.advance() {
15784            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15785            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15786        }
15787        let parent_table = self.expect_ident_like()?;
15788        let mut parent_columns: Vec<String> = Vec::new();
15789        if matches!(self.peek(), Token::LParen) {
15790            self.advance();
15791            loop {
15792                parent_columns.push(self.expect_ident_like()?);
15793                match self.peek() {
15794                    Token::Comma => {
15795                        self.advance();
15796                    }
15797                    Token::RParen => {
15798                        self.advance();
15799                        break;
15800                    }
15801                    other => {
15802                        return Err(self.err(format!(
15803                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
15804                        )));
15805                    }
15806                }
15807            }
15808        }
15809        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15810            return Err(self.err(format!(
15811                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15812                expected_arity,
15813                parent_columns.len()
15814            )));
15815        }
15816        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15817        // it between the referenced column list and the ON / DEFERRABLE
15818        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15819        // is skipped when any referencing column is NULL), so SIMPLE —
15820        // the default, and the only spelling pg_dump emits — is accepted
15821        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15822        // mixed-NULL rule, which is not wired yet; reject them honestly
15823        // rather than silently applying SIMPLE (PG itself errors on
15824        // MATCH PARTIAL as "not yet implemented").
15825        let mut match_type = crate::ast::MatchType::Simple;
15826        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15827            self.advance();
15828            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15829            // SIMPLE / PARTIAL arrive as bare identifiers.
15830            let kind = match self.advance() {
15831                Token::Full => "FULL".to_string(),
15832                Token::Ident(s) => s.to_uppercase(),
15833                other => {
15834                    return Err(self.err(format!(
15835                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15836                    )));
15837                }
15838            };
15839            match kind.as_str() {
15840                "SIMPLE" => {} // Default — match_type stays Simple.
15841                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15842                // when ALL referencing columns are NULL; a mixed-NULL key errors.
15843                "FULL" => match_type = crate::ast::MatchType::Full,
15844                "PARTIAL" => {
15845                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15846                }
15847                _ => {
15848                    return Err(self.err(format!(
15849                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15850                    )));
15851                }
15852            }
15853        }
15854        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15855        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15856        // <action>` / `ON UPDATE <action>` in either order. PG /
15857        // pg_dump emits the timing clause AFTER the ON clauses
15858        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15859        // but the SQL spec allows either order. We loop over
15860        // every possible trailer and dispatch on the next token,
15861        // stopping when nothing matches. Phase 3.1 changes the
15862        // bare DEFERRABLE form from hard-error to accept-as-
15863        // immediate; SPG is single-writer with no deferred-
15864        // constraint window so the runtime semantics are always
15865        // immediate even when INITIALLY DEFERRED is requested.
15866        // PG's default referential action (no ON DELETE / ON UPDATE
15867        // clause) is NO ACTION, not RESTRICT — the two enforce
15868        // identically in SPG (single-writer, no deferred window; see the
15869        // shared match arm in constraints.rs) but information_schema.
15870        // referential_constraints must report NO ACTION to match PG.
15871        let mut on_delete = FkAction::NoAction;
15872        let mut on_update = FkAction::NoAction;
15873        let mut seen_on_delete = false;
15874        let mut seen_on_update = false;
15875        let mut deferrable = false;
15876        let mut initially_deferred = false;
15877        loop {
15878            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15879            let before = self.pos;
15880            let (d, idef) = self.consume_deferrable_clauses_timed()?;
15881            if self.pos != before {
15882                deferrable = d;
15883                initially_deferred = idef;
15884                continue;
15885            }
15886            // ON DELETE / ON UPDATE.
15887            if !matches!(self.peek(), Token::On) {
15888                break;
15889            }
15890            self.advance();
15891            let which = self.advance();
15892            let action = self.parse_fk_action()?;
15893            match which {
15894                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15895                    if seen_on_delete {
15896                        return Err(self.err("ON DELETE specified twice".into()));
15897                    }
15898                    seen_on_delete = true;
15899                    on_delete = action;
15900                }
15901                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15902                    if seen_on_update {
15903                        return Err(self.err("ON UPDATE specified twice".into()));
15904                    }
15905                    seen_on_update = true;
15906                    on_update = action;
15907                }
15908                other => {
15909                    return Err(
15910                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15911                    );
15912                }
15913            }
15914        }
15915        Ok((
15916            parent_table,
15917            parent_columns,
15918            on_delete,
15919            on_update,
15920            match_type,
15921            deferrable,
15922            initially_deferred,
15923        ))
15924    }
15925
15926    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15927    /// NO ACTION`.
15928    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15929        match self.advance() {
15930            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15931            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15932            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15933                Token::Null => Ok(FkAction::SetNull),
15934                Token::Default => Ok(FkAction::SetDefault),
15935                other => Err(self.err(format!(
15936                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15937                ))),
15938            },
15939            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15940                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15941                other => Err(self.err(format!(
15942                    "expected ACTION after NO in FK action, got {other:?}"
15943                ))),
15944            },
15945            other => Err(self.err(format!(
15946                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15947            ))),
15948        }
15949    }
15950
15951    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15952    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15953    fn consume_if_not_exists(&mut self) -> bool {
15954        // `IF` arrives as a bare Ident (we don't reserve it because it
15955        // also appears mid-expression in PG, though we don't support
15956        // those forms yet).
15957        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15958        if !looks_like_if {
15959            return false;
15960        }
15961        // Peek one ahead before committing: only consume IF when it's
15962        // actually `IF NOT EXISTS`.
15963        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15964            return false;
15965        }
15966        if !matches!(
15967            self.tokens.get(self.pos + 2),
15968            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15969        ) {
15970            return false;
15971        }
15972        self.advance(); // IF
15973        self.advance(); // NOT
15974        self.advance(); // EXISTS
15975        true
15976    }
15977
15978    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15979    /// Consumes IF EXISTS as a pair; returns false otherwise
15980    /// without consuming any tokens.
15981    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15982    /// ENABLE/DISABLE/FORCE/NO FORCE.
15983    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15984        for kw in ["row", "level", "security"] {
15985            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15986            {
15987                return Err(self.err(alloc::format!(
15988                    "expected {} in ROW LEVEL SECURITY, got {:?}",
15989                    kw.to_ascii_uppercase(),
15990                    self.peek()
15991                )));
15992            }
15993            self.advance();
15994        }
15995        Ok(())
15996    }
15997
15998    fn consume_if_exists(&mut self) -> bool {
15999        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16000        if !looks_like_if {
16001            return false;
16002        }
16003        if !matches!(
16004            self.tokens.get(self.pos + 1),
16005            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16006        ) {
16007            return false;
16008        }
16009        self.advance(); // IF
16010        self.advance(); // EXISTS
16011        true
16012    }
16013
16014    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
16015    /// qualifiers after an index column ref. ASC / DESC are
16016    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
16017    /// We accept and discard them since single-column BTree
16018    /// stores rows in natural key order today.
16019    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
16020    /// ORDER BY key. Returns None when absent.
16021    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
16022        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16023            return Ok(None);
16024        }
16025        self.advance();
16026        match self.advance() {
16027            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
16028            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
16029            other => Err(self.err(alloc::format!(
16030                "expected FIRST or LAST after NULLS, got {other:?}"
16031            ))),
16032        }
16033    }
16034
16035    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
16036    /// rather than discarded.
16037    ///
16038    /// SPG's index does not scan in a direction — column ordering is
16039    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
16040    /// reproduction of the DDL, and dropping the clause meant
16041    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
16042    /// dump lost it, and a schema diff saw drift on every run.
16043    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
16044        let mut order = crate::ast::IndexColumnOrder::default();
16045        loop {
16046            match self.peek() {
16047                Token::Asc => {
16048                    self.advance();
16049                }
16050                Token::Desc => {
16051                    order.descending = true;
16052                    self.advance();
16053                }
16054                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
16055                    let look = self.tokens.get(self.pos + 1);
16056                    if matches!(
16057                        look,
16058                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
16059                            || k.eq_ignore_ascii_case("last")
16060                    ) {
16061                        self.advance();
16062                        order.nulls_first = Some(matches!(
16063                            self.advance(),
16064                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
16065                        ));
16066                    } else {
16067                        break;
16068                    }
16069                }
16070                _ => break,
16071            }
16072        }
16073        order
16074    }
16075
16076    fn parse_create_index_stmt_after_create(
16077        &mut self,
16078        is_unique: bool,
16079    ) -> Result<Statement, ParseError> {
16080        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
16081        debug_assert!(matches!(self.peek(), Token::Index));
16082        self.advance();
16083        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
16084        // SPG's CREATE INDEX is synchronous end-to-end today (real
16085        // CONCURRENTLY variant with restartable scans queues with
16086        // v7.39 indexes epic), so the modifier has no runtime effect
16087        // — same accept-and-no-op shape as v7.37.16.5 DETACH
16088        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
16089        // VIEW CONCURRENTLY.
16090        let mut concurrently = false;
16091        if matches!(
16092            self.peek(),
16093            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
16094        ) {
16095            self.advance();
16096            concurrently = true;
16097        }
16098        let if_not_exists = self.consume_if_not_exists();
16099        // v7.39 (read01 round 93) — the index name is optional (PG since
16100        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
16101        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
16102        // was given; leave it empty and the engine derives a PG-style
16103        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
16104        let name = if matches!(self.peek(), Token::On) {
16105            String::new()
16106        } else {
16107            self.expect_ident_like()?
16108        };
16109        if !matches!(self.peek(), Token::On) {
16110            return Err(self.err(format!(
16111                "expected ON after CREATE INDEX <name>, got {:?}",
16112                self.peek()
16113            )));
16114        }
16115        self.advance();
16116        let table = self.expect_ident_like()?;
16117        // Optional `USING <method>` — only recognised method in v2.0 is
16118        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
16119        // ident `using` (we don't promote it to a reserved keyword
16120        // because it isn't reserved anywhere else in our SQL surface).
16121        let mut method_name: Option<String> = None;
16122        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
16123            self.advance();
16124            let m = self.expect_ident_like()?;
16125            method_name = Some(m.to_ascii_lowercase());
16126            match m.to_ascii_lowercase().as_str() {
16127                "hnsw" => IndexMethod::Hnsw,
16128                "btree" => IndexMethod::BTree,
16129                "brin" => IndexMethod::Brin,
16130                // v7.12.3 — real GIN inverted index over `tsvector`.
16131                // v7.9.26b's `USING gin` → BTree silent fallback is
16132                // gone; the engine validates that the indexed column
16133                // is `tsvector` at CREATE INDEX time.
16134                "gin" => IndexMethod::Gin,
16135                // v7.9.26b — PG `pg_dump` emits `USING gist` /
16136                // `USING spgist` / `USING hash` for their built-in
16137                // AMs that SPG doesn't have a matching
16138                // implementation for; degrade to BTree on the
16139                // leading column so the schema loads + the index
16140                // catalogue stays consistent. Operator pays the
16141                // planner cost only for the queries that would have
16142                // used the specialised AM.
16143                "gist" | "spgist" | "hash" => IndexMethod::BTree,
16144                // v7.11.3 — pgvector ships both `ivfflat` and
16145                // `hnsw`. Customers shouldn't have to choose
16146                // their on-disk index method based on what SPG
16147                // implements; accept `ivfflat` as a synonym for
16148                // `hnsw` so PG schemas using either method drop
16149                // in. The vector distance op (`<->` / `<#>` /
16150                // `<=>`) at query time still picks the metric.
16151                "ivfflat" => IndexMethod::Hnsw,
16152                other => {
16153                    return Err(self.err(alloc::format!(
16154                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
16155                    )));
16156                }
16157            }
16158        } else {
16159            IndexMethod::BTree
16160        };
16161        if !matches!(self.peek(), Token::LParen) {
16162            return Err(self.err(format!(
16163                "expected '(' before indexed column, got {:?}",
16164                self.peek()
16165            )));
16166        }
16167        self.advance();
16168        // v6.8.2 — accept either a bare column ident (legacy) or
16169        // an expression `fn(col, …)` for expression indexes.
16170        // Distinguish by peeking the token *after* the current
16171        // ident: `ident )` is the legacy column-only path;
16172        // anything else triggers the Pratt expression parser.
16173        // (`advance()` uses `mem::replace` to nil out the current
16174        // slot, so we can't save+rewind cleanly — peek-ahead via
16175        // direct index avoids the mutation.)
16176        let mut opclass: Option<String> = None;
16177        let mut key_collation: Option<String> = None;
16178        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16179            // Single column with `)` immediately after — fast path.
16180            // v7.9.29 — also: bare column followed by `,` (the
16181            // multi-column form `(a, b, c)`). Without this branch
16182            // the leading ident gets pulled into `parse_expr`
16183            // which then sets `expression = Some(Column(a))` and
16184            // breaks Display round-trip on the multi-column shape.
16185            Token::Ident(s) | Token::QuotedIdent(s)
16186                if matches!(
16187                    self.tokens.get(self.pos + 1),
16188                    Some(Token::RParen | Token::Comma)
16189                ) =>
16190            {
16191                self.advance();
16192                (s, None)
16193            }
16194            // v7.9.22 — single column followed by a pgvector
16195            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16196            // v7.15.0 — capture the opclass instead of discarding
16197            // it so the engine can dispatch (e.g. `gin_trgm_ops`
16198            // → real trigram-shingle GIN over a TEXT column).
16199            // Vector/HNSW opclasses still take their distance
16200            // metric from the query operator (`<->` / `<#>` /
16201            // `<=>`), so for those callers the opclass stays
16202            // informational.
16203            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16204            // opclass: `(embedding public.vector_cosine_ops)`. Strip
16205            // the schema and dispatch on the bare opclass, the same
16206            // treatment table/type names get.
16207            Token::Ident(s) | Token::QuotedIdent(s)
16208                if matches!(
16209                    self.tokens.get(self.pos + 1),
16210                    Some(Token::Ident(_) | Token::QuotedIdent(_))
16211                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16212                    && matches!(
16213                        self.tokens.get(self.pos + 3),
16214                        Some(Token::Ident(op) | Token::QuotedIdent(op))
16215                            if is_vector_opclass_name(op)
16216                    ) =>
16217            {
16218                self.advance(); // column name
16219                self.advance(); // schema qualifier
16220                self.advance(); // dot
16221                let op_tok = self.advance();
16222                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16223                    opclass = Some(op.to_ascii_lowercase());
16224                }
16225                (s, None)
16226            }
16227            // r1038 — an operator class is recognised by its POSITION, not
16228            // by a list of names. It used to be `is_vector_opclass_name`,
16229            // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16230            // sentori's migration wrote — was a syntax error while
16231            // `USING gin (doc)` parsed. Anything sitting between a column
16232            // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16233            // two bare identifiers in a row are not valid there otherwise.
16234            Token::Ident(s) | Token::QuotedIdent(s)
16235                if matches!(
16236                    self.tokens.get(self.pos + 1),
16237                    Some(Token::Ident(op) | Token::QuotedIdent(op))
16238                        if is_vector_opclass_name(op) || Self::opclass_position_follows(
16239                            self.tokens.get(self.pos + 2)
16240                        )
16241                ) =>
16242            {
16243                self.advance(); // column name
16244                // Capture the opclass token, lower-cased for
16245                // case-insensitive engine dispatch.
16246                let op_tok = self.advance();
16247                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16248                    opclass = Some(op.to_ascii_lowercase());
16249                }
16250                (s, None)
16251            }
16252            Token::Ident(_) | Token::QuotedIdent(_) => {
16253                // v7.39 (round 538) — an explicit COLLATE on the key,
16254                // read by LOOKAHEAD because `parse_expr` absorbs the
16255                // clause as a no-op (SPG orders text by bytes, which is
16256                // the C collation, so it changes nothing to honour). PG
16257                // still PRINTS it: an explicitly written `"C"` and the
16258                // collation a column inherits are different collation
16259                // OBJECTS even where they sort identically, which is why
16260                // `(a COLLATE "C")` shows on a C-collation database too.
16261                if matches!(
16262                    self.tokens.get(self.pos + 1),
16263                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16264                ) {
16265                    key_collation = match self.tokens.get(self.pos + 2) {
16266                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16267                            Some(n.clone())
16268                        }
16269                        _ => None,
16270                    };
16271                }
16272                // v7.39.2 — the clause is read by the LOOKAHEAD above and
16273                // belongs to the KEY, not to the expression. Since
16274                // `COLLATE` became a node, letting `parse_expr` build one
16275                // here put the collation in twice and the key deparsed as
16276                // `(c COLLATE "C" COLLATE "C")`. The ORDER-BY-key channel
16277                // is the same idea and already exists, so this borrows it:
16278                // absorb into the side channel, and the key's own
16279                // lookahead is what carries it.
16280                // v7.39.2 — and the key can only CARRY the byte-order
16281                // spellings. Absorbing into the side channel accepts any
16282                // name, so suppressing the node here without this check
16283                // silently accepted `(name COLLATE "en_US")`, which SPG's
16284                // index cannot honour — a refusal that was doing real
16285                // work, removed by the suppression and put back here.
16286                if let Some(name) = &key_collation {
16287                    let lc = name.to_ascii_lowercase();
16288                    let byte_order = matches!(
16289                        lc.as_str(),
16290                        "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
16291                    );
16292                    let mysql_ok = self.mysql_dialect
16293                        && (lc.ends_with("_ci")
16294                            || lc.ends_with("_bin")
16295                            || lc == "binary"
16296                            || matches!(lc.as_str(), "case_insensitive" | "nocase"));
16297                    if !byte_order && !mysql_ok {
16298                        return Err(self.err(alloc::format!(
16299                            "COLLATE {name:?} is not supported in this position: an index \
16300                             key carries the byte-order spellings only. Declare it on the \
16301                             column (`x text COLLATE {name:?}`) instead"
16302                        )));
16303                    }
16304                }
16305                let saved_key_ctx = self.in_order_by_key;
16306                self.in_order_by_key = true;
16307                let key_expr = self.parse_expr(0);
16308                self.in_order_by_key = saved_key_ctx;
16309                let key_expr = key_expr?;
16310                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16311                    self.err("expression index key must reference at least one column".into())
16312                })?;
16313                (primary, Some(key_expr))
16314            }
16315            // v7.37.43-T4 — parenthesised expression index key
16316            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16317            // PG's CREATE INDEX requires the expression to be in
16318            // its own parens to disambiguate function calls from
16319            // column lists, so this `LParen` is the inner open-paren
16320            // of an expression key. parse_expr handles the recursive
16321            // descent and consumes the matching `RParen`.
16322            Token::LParen => {
16323                let key_expr = self.parse_expr(0)?;
16324                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16325                    self.err("expression index key must reference at least one column".into())
16326                })?;
16327                (primary, Some(key_expr))
16328            }
16329            other => {
16330                return Err(self.err(format!(
16331                    "expected column ident or expression, got {other:?}"
16332                )));
16333            }
16334        };
16335        // v7.9.14 — accept extra comma-separated columns inside
16336        // the index key parens (`CREATE INDEX … (a, b, c)`).
16337        // mailrs F2. Each extra column may carry an optional
16338        // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
16339        // — parsed and discarded; SPG doesn't honour direction
16340        // on a BTree index today (column ordering is intrinsic
16341        // to the storage). v7.10 will widen to genuine composite
16342        // index keys.
16343        let mut extra_columns: Vec<String> = Vec::new();
16344        // The leading column may also have ASC/DESC after it — and that
16345        // one is the column SPG indexes, so its clause is kept.
16346        let key_order = self.consume_optional_index_column_qualifiers();
16347        while matches!(self.peek(), Token::Comma) {
16348            self.advance();
16349            let extra = self.expect_ident_like()?;
16350            let _ = self.consume_optional_index_column_qualifiers();
16351            extra_columns.push(extra);
16352        }
16353        if !matches!(self.peek(), Token::RParen) {
16354            return Err(self.err(format!(
16355                "expected ')' after indexed column / expression, got {:?}",
16356                self.peek()
16357            )));
16358        }
16359        self.advance();
16360        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16361        // index-only-scan annotation. Bare ident (not a reserved
16362        // keyword) so we test by case-insensitive string match.
16363        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16364        {
16365            self.advance();
16366            if !matches!(self.peek(), Token::LParen) {
16367                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16368            }
16369            self.advance();
16370            let mut cols = Vec::new();
16371            loop {
16372                cols.push(self.expect_ident_like()?);
16373                match self.peek() {
16374                    Token::Comma => {
16375                        self.advance();
16376                    }
16377                    Token::RParen => {
16378                        self.advance();
16379                        break;
16380                    }
16381                    other => {
16382                        return Err(self.err(format!(
16383                            "expected ',' or ')' in INCLUDE list, got {other:?}"
16384                        )));
16385                    }
16386                }
16387            }
16388            cols
16389        } else {
16390            Vec::new()
16391        };
16392        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16393        // storage parameters. pgvector emits `WITH (lists = N)` for
16394        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16395        // SPG's HNSW picks its own parameters today (tunable via
16396        // env vars), so the WITH clause is informational and dropped.
16397        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16398            self.advance();
16399            if !matches!(self.peek(), Token::LParen) {
16400                return Err(self.err(format!(
16401                    "expected '(' after WITH in CREATE INDEX, got {:?}",
16402                    self.peek()
16403                )));
16404            }
16405            self.advance();
16406            loop {
16407                if matches!(self.peek(), Token::RParen) {
16408                    self.advance();
16409                    break;
16410                }
16411                // Drain `key = value` or bare `key` tokens.
16412                let _ = self.advance(); // key
16413                if matches!(self.peek(), Token::Eq) {
16414                    self.advance();
16415                    let _ = self.advance(); // value (int / string / ident)
16416                }
16417                match self.peek() {
16418                    Token::Comma => {
16419                        self.advance();
16420                    }
16421                    Token::RParen => {
16422                        self.advance();
16423                        break;
16424                    }
16425                    other => {
16426                        return Err(self.err(format!(
16427                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
16428                        )));
16429                    }
16430                }
16431            }
16432        }
16433        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16434        // which sits between the key list and the WHERE clause.
16435        let mut nulls_not_distinct = false;
16436        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16437            let n1 = self.tokens.get(self.pos + 1);
16438            let n2 = self.tokens.get(self.pos + 2);
16439            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16440                self.advance(); // NULLS
16441                self.advance(); // NOT
16442                self.advance(); // DISTINCT
16443                nulls_not_distinct = true;
16444            } else if matches!(n1, Some(Token::Distinct)) {
16445                self.advance(); // NULLS
16446                self.advance(); // DISTINCT
16447            }
16448        }
16449        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16450        let partial_predicate = if matches!(self.peek(), Token::Where) {
16451            self.advance();
16452            Some(self.parse_expr(0)?)
16453        } else {
16454            None
16455        };
16456        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16457        // sense: uniqueness over an ANN structure has no clean
16458        // semantics. Reject early. (BRIN UNIQUE is similarly
16459        // meaningless — block both.)
16460        if is_unique && !matches!(method, IndexMethod::BTree) {
16461            return Err(self.err(alloc::format!(
16462                "UNIQUE is only supported on BTree indexes, got USING {:?}",
16463                method
16464            )));
16465        }
16466        Ok(Statement::CreateIndex(CreateIndexStatement {
16467            concurrently,
16468            name,
16469            key_order,
16470            key_collation,
16471            table,
16472            column,
16473            nulls_not_distinct,
16474            method,
16475            if_not_exists,
16476            included_columns,
16477            partial_predicate,
16478            extra_columns: extra_columns.clone(),
16479            expression,
16480            is_unique,
16481            opclass,
16482            method_name,
16483        }))
16484    }
16485
16486    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16487    /// column-level `REFERENCES ...` clause. The trailing FK is
16488    /// normalised into table-level shape (single-element columns +
16489    /// parent_columns) so the engine sees one uniform constraint list.
16490    fn parse_column_def_with_fk(
16491        &mut self,
16492    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16493        let col = self.parse_column_def()?;
16494        // v7.39 (round 308, V29) — an explicitly named inline FK:
16495        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16496        // loop leaves this spelling intact precisely so the name can be
16497        // kept here; PG reports it in violation messages and matches it
16498        // in `SET CONSTRAINTS`.
16499        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16500        {
16501            self.advance();
16502            Some(self.expect_ident_like()?)
16503        } else {
16504            None
16505        };
16506        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16507        let inline_references = matches!(
16508            self.peek(),
16509            Token::Ident(s) if s.eq_ignore_ascii_case("references")
16510        );
16511        if !inline_references {
16512            return Ok((col, None));
16513        }
16514        let (
16515            parent_table,
16516            parent_columns,
16517            on_delete,
16518            on_update,
16519            match_type,
16520            deferrable,
16521            initially_deferred,
16522        ) = self.parse_references_tail(1)?;
16523        let fk = ForeignKeyConstraint {
16524            name: declared_name,
16525            columns: vec![col.name.clone()],
16526            parent_table,
16527            parent_columns,
16528            on_delete,
16529            on_update,
16530            match_type,
16531            deferrable,
16532            initially_deferred,
16533        };
16534        Ok((col, Some(fk)))
16535    }
16536
16537    /// v7.13.0 — parse a column type (consuming the type ident and
16538    /// any trailing parameters / `[]`), without surrounding column
16539    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16540    /// Returns the resolved `ColumnTypeName` plus implied
16541    /// `(auto_increment, not_null)` flags from PG SERIAL family
16542    /// shorthands — callers that don't expect those (ALTER COLUMN
16543    /// TYPE) can discard them.
16544    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16545        let (ty, _, _, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16546        Ok(ty)
16547    }
16548
16549    #[allow(clippy::type_complexity)]
16550    fn parse_type_with_implied_flags(
16551        &mut self,
16552    ) -> Result<
16553        (
16554            ColumnTypeName,
16555            bool,
16556            bool,
16557            Option<String>,
16558            Collation,
16559            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16560            bool,
16561            // v7.39 (round 676) — the collation NAME as written, which the
16562            // `Collation` enum above cannot carry.
16563            Option<String>,
16564            bool,
16565            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16566            // list captured at type-parse time. None for all
16567            // non-ENUM types.
16568            Option<Vec<String>>,
16569            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16570            // list. Distinct from ENUM (subset semantics).
16571            Option<Vec<String>>,
16572            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16573            // width, lost when the type collapses to SmallInt / Int.
16574            Option<MysqlIntWidth>,
16575            // v7.39 (round 424) — declared fractional-seconds precision of a
16576            // MySQL temporal column (bare spelling = 0). None under PG.
16577            Option<u8>,
16578            // v7.39.2 — written `TIMESTAMP` rather than `DATETIME`. The
16579            // two are different types on MySQL and SPG stores both as
16580            // `Timestamp`, so the spelling has to travel separately or
16581            // a dump silently rewrites the column.
16582            bool,
16583            // v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair. Not a
16584            // display hint: it rounds on write.
16585            Option<(u8, u8)>,
16586        ),
16587        ParseError,
16588    > {
16589        let mut ty_ident = match self.advance() {
16590            Token::Ident(s) => s,
16591            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16592            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16593            // '<span>'` literal grammar. As a column type it lands
16594            // here directly; downstream resolution still uses the
16595            // canonical lowercase string.
16596            Token::Interval => "interval".to_string(),
16597            other => {
16598                return Err(ParseError {
16599                    message: format!("expected column type, got {other:?}"),
16600                    token_pos: self.consumed_pos(),
16601                });
16602            }
16603        };
16604        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16605        // pg_dump qualifies extension types (`public.vector(1024)`).
16606        // SPG is single-namespace; drop the schema and resolve the
16607        // bare type — same treatment table names already get.
16608        while matches!(self.peek(), Token::Dot) {
16609            self.advance();
16610            ty_ident = self.expect_ident_like()?;
16611        }
16612        let mut implied_auto_increment = false;
16613        let mut implied_not_null = false;
16614        let mut user_type_ref: Option<String> = None;
16615        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16616        // value list, captured here and bubbled up through the
16617        // ColumnDef so the engine can attach it to the column
16618        // schema (and validate INSERT cells against it).
16619        let mut inline_enum_variants: Option<Vec<String>> = None;
16620        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16621        let mut inline_set_variants: Option<Vec<String>> = None;
16622        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16623        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16624        // collapses to SmallInt / Int. Only under the MySQL dialect.
16625        let mut mysql_int_width: Option<MysqlIntWidth> = None;
16626        // v7.39 (round 424) — the declared fractional-seconds precision of a
16627        // MySQL temporal column. Set by the temporal arms below; stays None
16628        // for PG (whose temporal columns keep full microseconds).
16629        let mut mysql_fsp: Option<u8> = None;
16630        let mut mysql_declared_timestamp = false;
16631        let mut mysql_float_md: Option<(u8, u8)> = None;
16632        let mut ty = match ty_ident.as_str() {
16633            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16634            "smallserial" | "serial2" => {
16635                implied_auto_increment = true;
16636                implied_not_null = true;
16637                ColumnTypeName::SmallInt
16638            }
16639            "serial" | "serial4" => {
16640                implied_auto_increment = true;
16641                implied_not_null = true;
16642                ColumnTypeName::Int
16643            }
16644            "bigserial" | "serial8" => {
16645                implied_auto_increment = true;
16646                implied_not_null = true;
16647                ColumnTypeName::BigInt
16648            }
16649            // MySQL flavours we accept by aliasing to the closest SPG
16650            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16651            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16652            // 24-bit) → INT. UNSIGNED modifiers are consumed below
16653            // without semantic effect.
16654            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16655            // PG's internal type names; pg_dump and hand-written PG schemas
16656            // use them interchangeably with smallint / int / bigint (the cast
16657            // path already accepted them, only the column grammar didn't).
16658            "smallint" | "int2" => {
16659                // v7.14.0 — MySQL display-width on integers
16660                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16661                // parenthesised number is purely cosmetic — it
16662                // doesn't change storage. Accept + discard.
16663                self.consume_optional_paren_size();
16664                ColumnTypeName::SmallInt
16665            }
16666            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16667            // canonical encoding for BOOLEAN. Every MySQL driver
16668            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16669            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16670            // 4.3 SPG classified TINYINT(1) as SmallInt, which
16671            // gave the customer i16-shaped values where the app
16672            // expected bool — a Tier-A silent type drift on
16673            // mysqldump restores. Now: `TINYINT(1)` → Bool;
16674            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16675            // stay SmallInt (the legacy width-agnostic path).
16676            "tinyint" => {
16677                let width = self.peek_optional_paren_size_value();
16678                self.consume_optional_paren_size();
16679                if width == Some(1) {
16680                    ColumnTypeName::Bool
16681                } else {
16682                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16683                    // lost width so the write path can enforce -128..127.
16684                    if self.mysql_dialect {
16685                        mysql_int_width = Some(MysqlIntWidth::Tiny);
16686                    }
16687                    ColumnTypeName::SmallInt
16688                }
16689            }
16690            "mediumint" => {
16691                self.consume_optional_paren_size();
16692                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16693                if self.mysql_dialect {
16694                    mysql_int_width = Some(MysqlIntWidth::Medium);
16695                }
16696                ColumnTypeName::Int
16697            }
16698            "int" | "integer" | "int4" => {
16699                self.consume_optional_paren_size();
16700                ColumnTypeName::Int
16701            }
16702            "bigint" | "int8" => {
16703                self.consume_optional_paren_size();
16704                ColumnTypeName::BigInt
16705            }
16706            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16707            // (mailrs round-5 G6). Consume the optional `PRECISION`
16708            // tail when the type keyword was `double` / `DOUBLE`.
16709            //
16710            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16711            // FLOAT". `FLOAT(p)` picks the width the way PG does:
16712            // p in 1..=24 is real, 25..=53 is double precision, and
16713            // anything else is an error.
16714            "float" | "double" | "real" => {
16715                if ty_ident.eq_ignore_ascii_case("double")
16716                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16717                {
16718                    self.advance();
16719                }
16720                if ty_ident.eq_ignore_ascii_case("real") {
16721                    // v7.39 (round 274) — the two dialects genuinely
16722                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16723                    // synonym for DOUBLE (8-byte). Round 269 made REAL
16724                    // 32-bit globally and thereby narrowed the stored
16725                    // precision of every MySQL REAL column.
16726                    if self.mysql_dialect {
16727                        ColumnTypeName::Float
16728                    } else {
16729                        ColumnTypeName::Real
16730                    }
16731                } else if self.mysql_dialect
16732                    && matches!(self.peek(), Token::LParen)
16733                    && self.peek_paren_has_comma()
16734                {
16735                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16736                    // display form (`FLOAT(10,2)`), which PG has no
16737                    // equivalent of. It was `syntax error at or near ","`,
16738                    // so the whole CREATE failed.
16739                    //
16740                    // v7.39.2 — the guard said `float` while the comment
16741                    // said both, so `DOUBLE(10,2)` — which every legacy
16742                    // MySQL schema uses for money — still failed the
16743                    // whole CREATE with `syntax error at or near "("`.
16744                    // Measured on 9.7.2: both forms are accepted, and the
16745                    // digits are NOT a display hint, they round on write
16746                    // (3.14159265358979 into either stores 3.14). The
16747                    // rounding is recorded as a residual; accepting the
16748                    // syntax and keeping the width is the half this
16749                    // change makes.
16750                    // v7.39.3 — keep the pair. The digits are not a
16751                    // display hint: MySQL 9.7.2 ROUNDS on write and
16752                    // refuses a value wider than `m` (errno 1264), so a
16753                    // column declared for money held more precision here
16754                    // than its schema said.
16755                    let (m, d) = self.parse_optional_numeric_params()?;
16756                    mysql_float_md = Some((
16757                        u8::try_from(m).unwrap_or(u8::MAX),
16758                        u8::try_from(d.max(0)).unwrap_or(u8::MAX),
16759                    ));
16760                    if ty_ident.eq_ignore_ascii_case("float") {
16761                        ColumnTypeName::Real
16762                    } else {
16763                        ColumnTypeName::Float
16764                    }
16765                } else if ty_ident.eq_ignore_ascii_case("float")
16766                    && matches!(self.peek(), Token::LParen)
16767                {
16768                    // PG words the two bounds differently, and
16769                    // parse_paren_size already rejects a zero.
16770                    let p = self.parse_paren_size("FLOAT")?;
16771                    if p > 53 {
16772                        return Err(self.err(String::from(
16773                            "precision for type float must be less than 54 bits",
16774                        )));
16775                    }
16776                    if p <= 24 {
16777                        ColumnTypeName::Real
16778                    } else {
16779                        ColumnTypeName::Float
16780                    }
16781                } else if ty_ident.eq_ignore_ascii_case("float") && self.mysql_dialect {
16782                    // v7.39.2 — MySQL's bare FLOAT is FOUR bytes; PG's is
16783                    // eight (it is `float8`'s spelling there). SPG used
16784                    // PG's for both, so a MySQL FLOAT column silently
16785                    // kept more precision than MySQL does — measured,
16786                    // 3.14159265358979 comes back as 3.14159 there and
16787                    // came back whole here — and reported itself as
16788                    // `double` to every reflection.
16789                    //
16790                    // This is the mirror of the REAL split above: the
16791                    // two dialects disagree about which spelling means
16792                    // which width, and one of them was already honoured.
16793                    ColumnTypeName::Real
16794                } else {
16795                    ColumnTypeName::Float
16796                }
16797            }
16798            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16799            "float4" => ColumnTypeName::Real,
16800            "float8" => ColumnTypeName::Float,
16801            "text" => ColumnTypeName::Text,
16802            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16803            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16804            // real MySQL schema and NONE of them existed: the CREATE
16805            // failed outright with `type "blob" does not exist`, so the
16806            // table was never made. The sizes differ only in MySQL's
16807            // maximum length, which SPG does not cap, so they collapse
16808            // onto TEXT and BYTEA the way the unsized spellings do.
16809            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16810            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16811            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16812            // enforce, consumed so the declaration parses.
16813            "varbinary" | "binary" => {
16814                self.consume_optional_paren_size();
16815                ColumnTypeName::Bytes
16816            }
16817            "name" => ColumnTypeName::Name,
16818            "xid" => ColumnTypeName::Xid,
16819            "oid" => ColumnTypeName::Oid,
16820            "xid8" => ColumnTypeName::Xid8,
16821            "bool" | "boolean" => ColumnTypeName::Bool,
16822            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16823            // an unbounded `character varying`, which the arm below has always
16824            // read as text. Only the short spelling demanded a length, so
16825            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16826            // there is — failed on `VARCHAR type requires (N)` while the long
16827            // spelling of the same thing was accepted. The same asymmetry
16828            // round 613 closed on the CAST side, here on the DDL side.
16829            "varchar" => {
16830                if matches!(self.peek(), Token::LParen) {
16831                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16832                } else {
16833                    ColumnTypeName::Text
16834                }
16835            }
16836            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16837            // `character` below (SQL standard).
16838            "char" => {
16839                if matches!(self.peek(), Token::LParen) {
16840                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16841                } else {
16842                    ColumnTypeName::Char(1)
16843                }
16844            }
16845            // pg_dump's canonical spellings: `character varying(n)` = varchar,
16846            // `character(n)` = char, bare `character` = char(1). Unbounded
16847            // `character varying` maps to text.
16848            "character" => {
16849                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16850                    self.advance();
16851                    if matches!(self.peek(), Token::LParen) {
16852                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16853                    } else {
16854                        ColumnTypeName::Text
16855                    }
16856                } else if matches!(self.peek(), Token::LParen) {
16857                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16858                } else {
16859                    ColumnTypeName::Char(1)
16860                }
16861            }
16862            "vector" => {
16863                let dim = self.parse_paren_size("VECTOR")?;
16864                let encoding = self.parse_optional_vector_encoding()?;
16865                ColumnTypeName::Vector { dim, encoding }
16866            }
16867            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16868            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16869            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16870            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16871            // DECIMAL(10,2))` — how nearly every money column is written,
16872            // in either dialect — was a syntax error and the table was
16873            // never created. `FIXED` is MySQL's alias alone, so it is
16874            // taken only in that dialect.
16875            "numeric" | "decimal" | "dec" => {
16876                let (precision, scale) = self.parse_optional_numeric_params()?;
16877                ColumnTypeName::Numeric(precision, scale)
16878            }
16879            "fixed" if self.mysql_dialect => {
16880                let (precision, scale) = self.parse_optional_numeric_params()?;
16881                ColumnTypeName::Numeric(precision, scale)
16882            }
16883            "date" => ColumnTypeName::Date,
16884            // MySQL's `DATETIME` is the same domain as standard
16885            // `TIMESTAMP` — accept both spellings.
16886            "timestamp" | "datetime" => {
16887                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16888                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16889                // TIME ZONE` clause, so consume it first.
16890                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16891                // (it truncates on write and pads on render), so capture it;
16892                // a bare spelling means precision 0 there. PG stores µs always
16893                // and keeps `None`.
16894                let n = self.take_optional_paren_size();
16895                if self.mysql_dialect {
16896                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16897                    // v7.39.2 — remember WHICH spelling was written.
16898                    // MySQL and MariaDB keep `timestamp` and `datetime`
16899                    // apart everywhere a client can read the type back,
16900                    // and SPG reported `datetime` for both — so a dump
16901                    // and reload silently changed the column's declared
16902                    // type, and MySQL's TIMESTAMP is not DATETIME (a
16903                    // different range, and UTC conversion on the way in
16904                    // and out).
16905                    mysql_declared_timestamp = ty_ident.eq_ignore_ascii_case("timestamp");
16906                }
16907                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16908                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16909                // the full form. SPG canonicalises:
16910                //   - WITH TIME ZONE    → Timestamptz
16911                //   - WITHOUT TIME ZONE → Timestamp
16912                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16913                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16914                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16915                {
16916                    self.advance(); // WITH
16917                    self.advance(); // TIME
16918                    self.advance(); // ZONE
16919                    ColumnTypeName::Timestamptz
16920                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16921                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16922                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16923                {
16924                    self.advance(); // WITHOUT
16925                    self.advance(); // TIME
16926                    self.advance(); // ZONE
16927                    ColumnTypeName::Timestamp
16928                } else {
16929                    // A second `(precision)` cannot legally follow, but the
16930                    // old grammar tolerated it; keep that tolerance.
16931                    self.consume_optional_paren_size();
16932                    ColumnTypeName::Timestamp
16933                }
16934            }
16935            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16936            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16937            // only PG-wire OID differs.
16938            "timestamptz" => {
16939                self.consume_optional_paren_size();
16940                ColumnTypeName::Timestamptz
16941            }
16942            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16943            // validation. We accept the JSONB spelling too because
16944            // most PG clients default to it; SPG doesn't distinguish
16945            // the two (no path-operator perf advantage to model).
16946            "json" => ColumnTypeName::Json,
16947            "jsonb" => ColumnTypeName::Jsonb,
16948            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16949            // surface here. Same storage shape; mapping happens at
16950            // the engine side via the ColumnTypeName → DataType
16951            // resolver. Literal forms are handled at coerce_value
16952            // time so the lexer stays untouched.
16953            "bytea" | "bytes" => ColumnTypeName::Bytes,
16954            // v7.17.0 Phase 7 — PG network address types
16955            // v7.17.0 had a Text-backed fallback here for
16956            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16957            // each to a first-class type; the keywords are
16958            // bound below in the ζ-A block.
16959            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16960            // The actual `to_tsvector` / `@@` / `ts_rank` surface
16961            // arrives in v7.12.1+; the type itself loads here so
16962            // mailrs's `scripts/init-schema.sql` runs unmodified.
16963            "tsvector" => ColumnTypeName::TsVector,
16964            "tsquery" => ColumnTypeName::TsQuery,
16965            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16966            // surface for Django / Rails / Hibernate's default
16967            // PK pattern.
16968            "uuid" => ColumnTypeName::Uuid,
16969            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16970            // Storage = three-field {months, days, micros}, catalog
16971            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16972            // line `INTERVAL` was parser-rejected at CREATE TABLE.
16973            "interval" => {
16974                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16975                // SECOND` and an optional `(p)` precision. SPG stores the full
16976                // {months,days,micros}; consume + ignore the qualifier/precision.
16977                while matches!(self.peek(), Token::To)
16978                    || matches!(self.peek(), Token::Ident(s) if matches!(
16979                        s.to_ascii_lowercase().as_str(),
16980                        "year" | "month" | "day" | "hour" | "minute" | "second"
16981                    ))
16982                {
16983                    self.advance();
16984                }
16985                self.consume_optional_paren_size();
16986                ColumnTypeName::Interval
16987            }
16988            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16989            // i64 microseconds since 00:00:00. Wire OID 1083.
16990            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16991            "time" => {
16992                // v7.39 (round 424) — MySQL TIME carries a semantic
16993                // fractional-seconds precision, bare meaning 0.
16994                let n = self.take_optional_paren_size();
16995                if self.mysql_dialect {
16996                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16997                }
16998                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16999                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17000                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17001                {
17002                    self.advance();
17003                    self.advance();
17004                    self.advance();
17005                    ColumnTypeName::TimeTz
17006                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17007                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17008                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17009                {
17010                    self.advance();
17011                    self.advance();
17012                    self.advance();
17013                    ColumnTypeName::Time
17014                } else {
17015                    ColumnTypeName::Time
17016                }
17017            }
17018            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
17019            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
17020            "year" => ColumnTypeName::Year,
17021            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
17022            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
17023            "timetz" => ColumnTypeName::TimeTz,
17024            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
17025            // Wire OID 790.
17026            "money" => ColumnTypeName::Money,
17027            // v7.17.0 Phase 3.P0-38 — PG range types.
17028            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
17029            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
17030            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
17031            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
17032            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
17033            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
17034            // v7.37.5 δ — PG 14+ multirange keywords.
17035            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
17036            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
17037            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
17038            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
17039            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
17040            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
17041            // v7.37.5 ε — PG geometry scalar keywords.
17042            "point" => ColumnTypeName::Point,
17043            "lseg" => ColumnTypeName::Lseg,
17044            "path" => ColumnTypeName::Path,
17045            "box" => ColumnTypeName::PgBox,
17046            "polygon" => ColumnTypeName::Polygon,
17047            "line" => ColumnTypeName::Line,
17048            "circle" => ColumnTypeName::Circle,
17049            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
17050            "inet" => ColumnTypeName::Inet,
17051            "cidr" => ColumnTypeName::Cidr,
17052            "macaddr" => ColumnTypeName::Macaddr,
17053            "macaddr8" => ColumnTypeName::Macaddr8,
17054            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
17055            // width in the value, so the optional `(N)` typmod is accepted and
17056            // ignored (the column stores whatever width it's given).
17057            "bit" => {
17058                let varying = matches!(
17059                    self.peek(),
17060                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
17061                );
17062                if varying {
17063                    self.advance();
17064                }
17065                // v7.39 (round 281) — the length used to be parsed and
17066                // dropped, so `bit(3)` accepted a five-bit string.
17067                let n = if matches!(self.peek(), Token::LParen) {
17068                    self.parse_paren_size("BIT")?
17069                } else {
17070                    0
17071                };
17072                if varying {
17073                    ColumnTypeName::BitVarying(n)
17074                } else {
17075                    ColumnTypeName::Bit(n)
17076                }
17077            }
17078            "varbit" => {
17079                let n = if matches!(self.peek(), Token::LParen) {
17080                    self.parse_paren_size("VARBIT")?
17081                } else {
17082                    0
17083                };
17084                ColumnTypeName::BitVarying(n)
17085            }
17086            "xml" => ColumnTypeName::Xml,
17087            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
17088            "hstore" => ColumnTypeName::Hstore,
17089            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
17090            // `ENUM('a','b','c')`. Storage is TEXT; the value
17091            // list lands on `inline_enum_variants` for the
17092            // engine to validate INSERT cells against. Empty
17093            // value list is a parse error (matches MySQL).
17094            "enum" => {
17095                // Expect the opening `(`.
17096                if !matches!(self.peek(), Token::LParen) {
17097                    return Err(self.err(alloc::format!(
17098                        "expected '(' after ENUM, got {:?}",
17099                        self.peek()
17100                    )));
17101                }
17102                self.advance();
17103                let mut variants: Vec<String> = Vec::new();
17104                loop {
17105                    match self.advance() {
17106                        Token::String(s) => variants.push(s),
17107                        other => {
17108                            return Err(self.err(alloc::format!(
17109                                "ENUM(...) expects string literal variants, got {other:?}"
17110                            )));
17111                        }
17112                    }
17113                    match self.peek() {
17114                        Token::Comma => {
17115                            self.advance();
17116                            continue;
17117                        }
17118                        Token::RParen => {
17119                            self.advance();
17120                            break;
17121                        }
17122                        other => {
17123                            return Err(self.err(alloc::format!(
17124                                "expected ',' or ')' in ENUM(...), got {other:?}"
17125                            )));
17126                        }
17127                    }
17128                }
17129                if variants.is_empty() {
17130                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
17131                }
17132                inline_enum_variants = Some(variants);
17133                // Storage is plain TEXT; the variant list lives on
17134                // the ColumnSchema side.
17135                ColumnTypeName::Text
17136            }
17137            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
17138            // `SET('a','b','c')`. Same parse shape as ENUM;
17139            // semantics differ (subset rather than pick-one).
17140            "set" => {
17141                if !matches!(self.peek(), Token::LParen) {
17142                    return Err(self.err(alloc::format!(
17143                        "expected '(' after SET, got {:?}",
17144                        self.peek()
17145                    )));
17146                }
17147                self.advance();
17148                let mut variants: Vec<String> = Vec::new();
17149                loop {
17150                    match self.advance() {
17151                        Token::String(s) => variants.push(s),
17152                        other => {
17153                            return Err(self.err(alloc::format!(
17154                                "SET(...) expects string literal variants, got {other:?}"
17155                            )));
17156                        }
17157                    }
17158                    match self.peek() {
17159                        Token::Comma => {
17160                            self.advance();
17161                            continue;
17162                        }
17163                        Token::RParen => {
17164                            self.advance();
17165                            break;
17166                        }
17167                        other => {
17168                            return Err(self.err(alloc::format!(
17169                                "expected ',' or ')' in SET(...), got {other:?}"
17170                            )));
17171                        }
17172                    }
17173                }
17174                if variants.is_empty() {
17175                    return Err(self.err("SET(...) must declare at least one variant".into()));
17176                }
17177                inline_set_variants = Some(variants);
17178                ColumnTypeName::Text
17179            }
17180            _other => {
17181                // v7.17.0 Phase 1.4 — unknown ident → defer
17182                // resolution to the engine. Stored as Text in
17183                // ColumnTypeName + the original name carried as
17184                // `user_type_ref` so CREATE TABLE can look up
17185                // user-defined enum / domain types.
17186                user_type_ref = Some(ty_ident.clone());
17187                ColumnTypeName::Text
17188            }
17189        };
17190        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
17191        // right after the type keyword. Pre-4.4 SPG consumed +
17192        // discarded the keyword, leaving a customer column
17193        // declared `id INT UNSIGNED NOT NULL` silently accepting
17194        // negative values — a Tier-A correctness drift where
17195        // application invariants (auto-increment-IDs never
17196        // negative) silently broke on cutover. Now: capture as
17197        // a column flag, persist on the schema, enforce at
17198        // INSERT / UPDATE time.
17199        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
17200        {
17201            self.advance();
17202            true
17203        } else {
17204            false
17205        };
17206        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
17207        // `<type> COLLATE <name>` post-fixes on text columns. SPG
17208        // stores text as UTF-8 always so CHARACTER SET is still a
17209        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
17210        // name: it gets classified into a `Collation` variant the
17211        // engine consults at WHERE-eval time. PG `default` /
17212        // `pg_catalog.default` / `C` / `POSIX` collations all
17213        // resolve to `Binary` (the prior behaviour); `_ci` /
17214        // `case_insensitive` / `nocase` shift to CaseInsensitive.
17215        // The schema-qualifier form (`pg_catalog.default`) lexes
17216        // as `Ident '.' Ident` — peek for the `.` and consume both
17217        // halves so it's treated as one collation name. PG's
17218        // `IDENT.IDENT` collation form (which can appear here) is
17219        // resolved by Collation::from_collation_name on the bare
17220        // identifier after the dot.
17221        let mut collation = Collation::Binary;
17222        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
17223        // clause was written. The engine needs this to tell an explicit
17224        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
17225        // clause at all: both resolve to `Collation::Binary`, but under the
17226        // MySQL dialect the latter takes the folding default collation.
17227        let mut collation_explicit = false;
17228        let mut collation_name: Option<alloc::string::String> = None;
17229        loop {
17230            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
17231                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
17232            {
17233                self.advance(); // CHARACTER
17234                self.advance(); // SET
17235                if matches!(
17236                    self.peek(),
17237                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
17238                ) {
17239                    self.advance();
17240                }
17241                continue;
17242            }
17243            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
17244                self.advance(); // COLLATE
17245                // Accept Ident / QuotedIdent / String AND the
17246                // keyword-tokenised `Default` (PG `pg_catalog.default`
17247                // and bare `DEFAULT` collation names — `default` is a
17248                // reserved word so the lexer hands back Token::Default
17249                // not Token::Ident).
17250                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
17251                    match this.peek().clone() {
17252                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
17253                            this.advance();
17254                            Some(s)
17255                        }
17256                        Token::Default => {
17257                            this.advance();
17258                            Some(alloc::string::String::from("default"))
17259                        }
17260                        _ => None,
17261                    }
17262                };
17263                let raw = if let Some(head) = read_collation_atom(self) {
17264                    // Schema-qualified PG form: `pg_catalog.default`.
17265                    if matches!(self.peek(), Token::Dot) {
17266                        self.advance();
17267                        let tail = read_collation_atom(self).unwrap_or_default();
17268                        alloc::format!("{head}.{tail}")
17269                    } else {
17270                        head
17271                    }
17272                } else {
17273                    alloc::string::String::new()
17274                };
17275                if !raw.is_empty() {
17276                    collation_explicit = true;
17277                    // v7.39 (round 676) — keep the name too. The enum below
17278                    // folds C / POSIX / en_US / default into one value, and
17279                    // `pg_attribute.attcollation` has to tell them apart.
17280                    // The schema qualifier goes: PG's `pg_catalog.default`
17281                    // and a bare `default` name the same collation.
17282                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17283                    // encoding suffix. Round 676 used `rsplit('.')` for
17284                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17285                    // PG writes `pg_catalog.default` (qualifier) and
17286                    // `en_US.utf8` (locale + encoding) with the same
17287                    // separator. Only `pg_catalog.` is a qualifier, and it
17288                    // is the only one PG's own dumps emit.
17289                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17290                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17291                    collation_name = Some(alloc::string::String::from(bare));
17292                    let parsed = Collation::from_collation_name(&raw);
17293                    // Last COLLATE clause wins, but `Binary` from a
17294                    // bare keyword like `default` should not
17295                    // silently downgrade a stronger one set earlier
17296                    // on the same column. v7.17 only ships one
17297                    // non-Binary variant so a simple OR is enough.
17298                    if parsed != Collation::Binary {
17299                        collation = parsed;
17300                    }
17301                }
17302                continue;
17303            }
17304            break;
17305        }
17306        // v7.10.10 — postfix `[]` widens the base type to its array
17307        // type. PG accepts `TYPE[]` after any base type and so does
17308        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17309        // all through; the old "only TEXT[]" note was stale).
17310        if matches!(self.peek(), Token::LBracket) {
17311            self.advance();
17312            if !matches!(self.peek(), Token::RBracket) {
17313                return Err(self.err(alloc::format!(
17314                    "TEXT[] takes no dimension; got {:?}",
17315                    self.peek()
17316                )));
17317            }
17318            self.advance();
17319            // v7.11.13 — widened to INT[] and BIGINT[] in addition
17320            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17321            // still error here.
17322            ty = match ty {
17323                ColumnTypeName::Text => ColumnTypeName::TextArray,
17324                ColumnTypeName::Int => ColumnTypeName::IntArray,
17325                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17326                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17327                // `[]` grammar. Wire OID 1187.
17328                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17329                // v7.37.5 γ — full PG array-of-scalar family.
17330                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17331                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17332                ColumnTypeName::Float => ColumnTypeName::FloatArray,
17333                // NUMERIC(p, s) loses its precision params at the
17334                // array level (matches PG: `NUMERIC[]` is untyped,
17335                // per-element precision flows through values).
17336                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17337                ColumnTypeName::Date => ColumnTypeName::DateArray,
17338                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17339                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17340                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17341                ColumnTypeName::Json => ColumnTypeName::JsonArray,
17342                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17343                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17344                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17345                // the array level (matches PG semantics where the
17346                // element precision is per-row, not column-wide).
17347                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17348                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17349                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17350                // follow-up.
17351                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17352                other => {
17353                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17354                }
17355            };
17356            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17357            // for INT/TEXT/BIGINT. Anything else is an error.
17358            if matches!(self.peek(), Token::LBracket) {
17359                self.advance();
17360                if !matches!(self.peek(), Token::RBracket) {
17361                    return Err(self.err(alloc::format!(
17362                        "TYPE[][] second dimension takes no size; got {:?}",
17363                        self.peek()
17364                    )));
17365                }
17366                self.advance();
17367                ty = match ty {
17368                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17369                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17370                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17371                    // v7.39 (read01 round 75) — bool[][].
17372                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17373                    other => {
17374                        return Err(self.err(alloc::format!(
17375                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17376                             TEXT[][] only; got {other:?}"
17377                        )));
17378                    }
17379                };
17380            }
17381        }
17382        Ok((
17383            ty,
17384            implied_auto_increment,
17385            implied_not_null,
17386            user_type_ref,
17387            collation,
17388            collation_explicit,
17389            collation_name,
17390            is_unsigned,
17391            inline_enum_variants,
17392            inline_set_variants,
17393            mysql_int_width,
17394            mysql_fsp,
17395            mysql_declared_timestamp,
17396            mysql_float_md,
17397        ))
17398    }
17399
17400    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17401        // v7.20 — PG reserves the table-constraint keywords, so a
17402        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17403        // malformed constraint clause (e.g. `UNIQUE a` missing its
17404        // parens), not a column named "unique". Since v7.17's
17405        // unknown-type leniency (`user_type_ref`) such a clause
17406        // would otherwise parse as a column with a user-defined
17407        // type — silently accepting invalid DDL. Quoted
17408        // identifiers ("unique" / `unique`) remain valid names.
17409        if let Token::Ident(s) = self.peek()
17410            && [
17411                "unique",
17412                "primary",
17413                "foreign",
17414                "constraint",
17415                "check",
17416                "references",
17417                "exclude",
17418            ]
17419            .iter()
17420            .any(|kw| s.eq_ignore_ascii_case(kw))
17421        {
17422            return Err(self.err(alloc::format!(
17423                "unexpected reserved keyword '{s}' at start of column definition \
17424                 (malformed table constraint?)"
17425            )));
17426        }
17427        let name_tok = self.pos;
17428        let name = self.expect_ident_like()?;
17429        // v7.39.3 — MySQL 9.7.2 reports a column by the SPELLING it was
17430        // declared with: `MyCol` stays `MyCol` in SHOW COLUMNS, in
17431        // information_schema, and in SHOW CREATE (measured). SPG folded
17432        // an unquoted name, so a table restored from a dump reported
17433        // names the application had never written.
17434        //
17435        // The written form comes back from the source span, which only
17436        // the MySQL dialect keeps. The span runs to the START of the
17437        // next token, so a comment or unusual spacing between them
17438        // arrives with it — hence the check that what came back is the
17439        // same identifier. It is not decoration: without it,
17440        // `CREATE TABLE t (MyCol /* c */ INT)` names the column
17441        // `MyCol /* c */`.
17442        let name = self
17443            .source_span(name_tok, name_tok)
17444            .map(|raw| raw.trim().trim_matches('`').trim_matches('"'))
17445            .filter(|raw| raw.eq_ignore_ascii_case(&name))
17446            .map_or(name, alloc::string::String::from);
17447        let (
17448            ty,
17449            implied_auto_increment,
17450            implied_not_null,
17451            user_type_ref,
17452            collation,
17453            collation_explicit,
17454            collation_name,
17455            is_unsigned,
17456            inline_enum_variants,
17457            inline_set_variants,
17458            mysql_int_width,
17459            mysql_fsp,
17460            mysql_declared_timestamp,
17461            mysql_float_md,
17462        ) = self.parse_type_with_implied_flags()?;
17463        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17464        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17465        // each at most once.
17466        let mut default: Option<Expr> = None;
17467        let mut nullable = !implied_not_null;
17468        let mut nullability_seen = implied_not_null;
17469        let mut auto_increment = implied_auto_increment;
17470        let mut is_primary_key = false;
17471        let mut is_unique = false;
17472        let mut unique_nulls_not_distinct = false;
17473        let mut constraint_deferrable = false;
17474        let mut constraint_initially_deferred = false;
17475        let mut check: Option<Expr> = None;
17476        let mut on_update_runtime: Option<Expr> = None;
17477        let mut generated_stored_expr: Option<Box<Expr>> = None;
17478        let mut identity_always = false;
17479        loop {
17480            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17481            // not-null constraints by name and pg_dump emits them
17482            // inline: `id bigint CONSTRAINT contacts_id_not_null1
17483            // NOT NULL`. Accept and discard the name; whatever
17484            // constraint follows is parsed by the arms below.
17485            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17486                // v7.39 (round 308, V29) — a name on an inline
17487                // REFERENCES belongs to the FOREIGN KEY, and the caller
17488                // (`parse_column_def_with_fk`) is what builds it, so
17489                // leave the whole clause for it. Dropping the name here
17490                // is what made `CONSTRAINT fk_a REFERENCES …` come back
17491                // as the synthesised `c_pid_fkey` — which then could
17492                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17493                // `advance()` takes tokens by `mem::replace`, so there
17494                // is no rewinding once consumed.
17495                if matches!(
17496                    self.tokens.get(self.pos + 2),
17497                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17498                ) {
17499                    break;
17500                }
17501                self.advance();
17502                let _name = self.expect_ident_like()?;
17503                continue;
17504            }
17505            // v7.39 (round 379) — MySQL's SHORT generated-column form
17506            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17507            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17508            // below), but hand-written schemas and app migrations use this.
17509            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17510            // SPG computes-and-stores either way, like the long form.
17511            if matches!(self.peek(), Token::As) {
17512                self.advance();
17513                if !matches!(self.peek(), Token::LParen) {
17514                    return Err(self.err(alloc::format!(
17515                        "expected '(' after AS in a generated column, got {:?}",
17516                        self.peek()
17517                    )));
17518                }
17519                self.advance();
17520                let expr = self.parse_expr(0)?;
17521                if !matches!(self.peek(), Token::RParen) {
17522                    return Err(self.err(alloc::format!(
17523                        "expected ')' after AS (<expr>), got {:?}",
17524                        self.peek()
17525                    )));
17526                }
17527                self.advance();
17528                if matches!(self.peek(), Token::Ident(s)
17529                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17530                {
17531                    self.advance();
17532                }
17533                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17534                continue;
17535            }
17536            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17537            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17538            // the modern replacement for SERIAL in hand-written
17539            // schemas). Both flavours map onto the auto-increment
17540            // machinery — SPG's serial semantics ≈ BY DEFAULT;
17541            // ALWAYS's reject-explicit-values nuance is documented
17542            // leniency. Generated EXPRESSION columns
17543            // (`AS (expr) STORED`) are not supported: error loudly
17544            // instead of silently storing NULLs.
17545            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17546                self.advance();
17547                let mut saw_generated_always = false;
17548                match self.peek().clone() {
17549                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17550                        self.advance();
17551                        saw_generated_always = true;
17552                    }
17553                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17554                        self.advance();
17555                        if !matches!(self.peek(), Token::Default) {
17556                            return Err(self.err(alloc::format!(
17557                                "expected DEFAULT after GENERATED BY, got {:?}",
17558                                self.peek()
17559                            )));
17560                        }
17561                        self.advance();
17562                    }
17563                    other => {
17564                        return Err(self.err(alloc::format!(
17565                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17566                        )));
17567                    }
17568                }
17569                if !matches!(self.peek(), Token::As) {
17570                    return Err(self.err(alloc::format!(
17571                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17572                        self.peek()
17573                    )));
17574                }
17575                self.advance();
17576                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17577                // ( <expr> ) STORED` stored computed-column. The
17578                // expression is captured for the engine to recompute
17579                // on every INSERT / UPDATE. v7.37.7 accepts the
17580                // STORED keyword only; PG also has VIRTUAL, which
17581                // v7.37.7 carves out (sentori only uses STORED).
17582                if matches!(self.peek(), Token::LParen) {
17583                    self.advance();
17584                    let expr = self.parse_expr(0)?;
17585                    if !matches!(self.peek(), Token::RParen) {
17586                        return Err(self.err(alloc::format!(
17587                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17588                            self.peek()
17589                        )));
17590                    }
17591                    self.advance();
17592                    let stored = match self.peek() {
17593                        Token::Ident(s) | Token::QuotedIdent(s)
17594                            if s.eq_ignore_ascii_case("stored") =>
17595                        {
17596                            self.advance();
17597                            true
17598                        }
17599                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17600                        // generated columns. SPG computes them on write and
17601                        // persists like STORED; the two are observably
17602                        // identical for query results (the value, recompute
17603                        // on base-column change, and NOT NULL enforcement all
17604                        // match), so a PG 18 schema/dump using VIRTUAL loads
17605                        // and behaves correctly. The compute-on-read storage
17606                        // saving is an invisible internal difference.
17607                        Token::Ident(s) | Token::QuotedIdent(s)
17608                            if s.eq_ignore_ascii_case("virtual") =>
17609                        {
17610                            self.advance();
17611                            false
17612                        }
17613                        other => {
17614                            return Err(self.err(alloc::format!(
17615                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17616                                 got {other:?}"
17617                            )));
17618                        }
17619                    };
17620                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
17621                    generated_stored_expr = Some(Box::new(expr));
17622                    continue;
17623                }
17624                self.expect_keyword_ident("identity")?;
17625                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17626                // consume the balanced parens and discard (SPG's
17627                // auto-increment is max+1-scan based).
17628                if matches!(self.peek(), Token::LParen) {
17629                    let mut depth = 0usize;
17630                    loop {
17631                        match self.advance() {
17632                            Token::LParen => depth += 1,
17633                            Token::RParen => {
17634                                depth -= 1;
17635                                if depth == 0 {
17636                                    break;
17637                                }
17638                            }
17639                            Token::Eof => {
17640                                return Err(self.err(
17641                                    "unterminated sequence-options parens after IDENTITY".into(),
17642                                ));
17643                            }
17644                            _ => {}
17645                        }
17646                    }
17647                }
17648                auto_increment = true;
17649                // v7.38 (read01) — remember the ALWAYS flavour so the engine
17650                // can reject explicit non-DEFAULT INSERT values (unless
17651                // OVERRIDING SYSTEM VALUE) the way PG does.
17652                identity_always = saw_generated_always;
17653                // PG identity columns are implicitly NOT NULL.
17654                nullable = false;
17655                continue;
17656            }
17657            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17658            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17659            // is accepted today. The "ON" token is an Ident
17660            // (not reserved) — peek before consuming.
17661            if matches!(self.peek(), Token::On)
17662                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17663            {
17664                self.advance(); // ON
17665                self.advance(); // update
17666                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17667                let next = self.peek().clone();
17668                match next {
17669                    Token::Ident(s) | Token::QuotedIdent(s)
17670                        if s.eq_ignore_ascii_case("current_timestamp") =>
17671                    {
17672                        self.advance();
17673                        // Optional `(N)` precision.
17674                        if matches!(self.peek(), Token::LParen) {
17675                            self.advance();
17676                            if !matches!(self.peek(), Token::Integer(_)) {
17677                                return Err(self.err(alloc::format!(
17678                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17679                                    self.peek()
17680                                )));
17681                            }
17682                            self.advance();
17683                            if !matches!(self.peek(), Token::RParen) {
17684                                return Err(self.err(alloc::format!(
17685                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17686                                    self.peek()
17687                                )));
17688                            }
17689                            self.advance();
17690                        }
17691                        on_update_runtime = Some(Expr::FunctionCall {
17692                            name: "now".into(),
17693                            args: Vec::new(),
17694                        });
17695                        continue;
17696                    }
17697                    other => {
17698                        return Err(self.err(alloc::format!(
17699                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17700                        )));
17701                    }
17702                }
17703            }
17704            if matches!(self.peek(), Token::Default) {
17705                if default.is_some() {
17706                    return Err(self.err("DEFAULT specified twice".into()));
17707                }
17708                self.advance();
17709                default = Some(self.parse_expr(0)?);
17710                continue;
17711            }
17712            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17713            // token with NOT NULL and sits EARLIER in the loop than the
17714            // deferrability arm, so without the lookahead it was reported as
17715            // "NOT NULL specified twice" (or "expected NULL after NOT").
17716            if matches!(self.peek(), Token::Not)
17717                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17718            {
17719                // NOT DEFERRABLE — explicit immediate; nothing to carry.
17720                self.consume_optional_deferrable_clauses()?;
17721                continue;
17722            }
17723            if matches!(self.peek(), Token::Not) {
17724                if nullability_seen {
17725                    return Err(self.err("NOT NULL specified twice".into()));
17726                }
17727                self.advance();
17728                if !matches!(self.peek(), Token::Null) {
17729                    return Err(self.err(format!(
17730                        "expected NULL after NOT in column def, got {:?}",
17731                        self.peek()
17732                    )));
17733                }
17734                self.advance();
17735                nullable = false;
17736                nullability_seen = true;
17737                continue;
17738            }
17739            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17740            // "this column is nullable" marker (the default in
17741            // standard SQL anyway). mysqldump emits it routinely
17742            // (`col TYPE NULL DEFAULT NULL` for nullable
17743            // timestamps etc). Accept + no-op.
17744            if matches!(self.peek(), Token::Null) {
17745                if nullability_seen && !nullable {
17746                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
17747                    // sentence, PG18-measured (the table name is the
17748                    // caller's; the column half is exact).
17749                    return Err(self.err(alloc::format!(
17750                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
17751                    )));
17752                }
17753                self.advance();
17754                nullable = true;
17755                nullability_seen = true;
17756                continue;
17757            }
17758            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17759            // arrives as a bare Ident. Match either, case-insensitive.
17760            if let Token::Ident(s) = self.peek()
17761                && (s.eq_ignore_ascii_case("auto_increment")
17762                    || s.eq_ignore_ascii_case("autoincrement"))
17763            {
17764                if auto_increment {
17765                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
17766                }
17767                self.advance();
17768                auto_increment = true;
17769                continue;
17770            }
17771            // v7.9.13 — inline `PRIMARY KEY` column constraint
17772            // (mailrs F1). Implies `NOT NULL`. The engine creates
17773            // a BTree index for the PK column at CREATE TABLE time
17774            // so FK parent-side index lookups resolve.
17775            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17776            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17777            // spelling was a parse error, so a pg_dump carrying one stopped
17778            // mid-restore. The clauses are consumed by the same helper the FK
17779            // path has used since round 288 and recorded nowhere: SPG enforces
17780            // the constraint IMMEDIATELY either way, which fails earlier than
17781            // PG inside a transaction that violates-then-repairs — a refusal,
17782            // not a wrong answer. True deferral is the open remainder of F08.
17783            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17784                || (matches!(self.peek(), Token::Not)
17785                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17786            {
17787                // v7.39 (round 711) — CARRIED now (the storing half of
17788                // F08); round 621 only consumed.
17789                let (d, idef) = self.consume_deferrable_clauses_timed()?;
17790                constraint_deferrable |= d;
17791                constraint_initially_deferred |= idef;
17792                continue;
17793            }
17794            if let Token::Ident(s) = self.peek()
17795                && s.eq_ignore_ascii_case("primary")
17796            {
17797                if is_primary_key {
17798                    return Err(self.err("PRIMARY KEY specified twice".into()));
17799                }
17800                // Peek-ahead for the required `KEY` token.
17801                let next = self.tokens.get(self.pos + 1);
17802                let next_is_key = matches!(
17803                    next,
17804                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17805                );
17806                if !next_is_key {
17807                    return Err(self.err(format!(
17808                        "expected KEY after PRIMARY in column def, got {:?}",
17809                        next
17810                    )));
17811                }
17812                self.advance(); // PRIMARY
17813                self.advance(); // KEY
17814                is_primary_key = true;
17815                if nullability_seen && nullable {
17816                    return Err(self.err(
17817                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17818                    ));
17819                }
17820                nullable = false;
17821                nullability_seen = true;
17822                continue;
17823            }
17824            // v7.13.0 — inline `UNIQUE` column constraint
17825            // (mailrs round-5 G2). Fold into a single-column
17826            // table-level UNIQUE at CREATE TABLE post-process time.
17827            if let Token::Ident(s) = self.peek()
17828                && s.eq_ignore_ascii_case("unique")
17829            {
17830                if is_unique {
17831                    return Err(self.err("UNIQUE specified twice".into()));
17832                }
17833                self.advance();
17834                is_unique = true;
17835                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17836                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17837                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17838                    let n1 = self.tokens.get(self.pos + 1);
17839                    let n2 = self.tokens.get(self.pos + 2);
17840                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17841                        self.advance(); // NULLS
17842                        self.advance(); // NOT
17843                        self.advance(); // DISTINCT
17844                        unique_nulls_not_distinct = true;
17845                    } else if matches!(n1, Some(Token::Distinct)) {
17846                        self.advance(); // NULLS
17847                        self.advance(); // DISTINCT
17848                    }
17849                }
17850                continue;
17851            }
17852            // v7.13.0 — inline `CHECK (<expr>)` column constraint
17853            // (mailrs round-5 G3). PG semantics: column-level
17854            // CHECK is equivalent to a table-level CHECK. Multiple
17855            // inline CHECKs on the same column AND together.
17856            if let Token::Ident(s) = self.peek()
17857                && s.eq_ignore_ascii_case("check")
17858            {
17859                self.advance();
17860                if !matches!(self.peek(), Token::LParen) {
17861                    return Err(self.err(alloc::format!(
17862                        "expected '(' after CHECK in column def, got {:?}",
17863                        self.peek()
17864                    )));
17865                }
17866                self.advance();
17867                let pred = self.parse_expr(0)?;
17868                if !matches!(self.peek(), Token::RParen) {
17869                    return Err(self.err(alloc::format!(
17870                        "expected ')' to close CHECK predicate, got {:?}",
17871                        self.peek()
17872                    )));
17873                }
17874                self.advance();
17875                check = Some(match check.take() {
17876                    Some(prev) => Expr::Binary {
17877                        op: BinOp::And,
17878                        lhs: Box::new(prev),
17879                        rhs: Box::new(pred),
17880                    },
17881                    None => pred,
17882                });
17883                continue;
17884            }
17885            break;
17886        }
17887        Ok(ColumnDef {
17888            name,
17889            ty,
17890            nullable,
17891            default,
17892            auto_increment,
17893            is_primary_key,
17894            is_unique,
17895            unique_nulls_not_distinct,
17896            constraint_deferrable,
17897            constraint_initially_deferred,
17898            check,
17899            user_type_ref,
17900            on_update_runtime,
17901            collation,
17902            collation_explicit,
17903            collation_name,
17904            is_unsigned,
17905            inline_enum_variants,
17906            inline_set_variants,
17907            generated_stored_expr,
17908            identity_always,
17909            mysql_int_width,
17910            mysql_fsp,
17911            mysql_declared_timestamp,
17912            mysql_float_md,
17913        })
17914    }
17915
17916    /// `NUMERIC` may appear without parameters, with one (precision
17917    /// only, scale=0), or with both. Returns `(precision, scale)` with
17918    /// 0 = unspecified for the bare form.
17919    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17920        if !matches!(self.peek(), Token::LParen) {
17921            // Bare `NUMERIC` — PG treats this as "unlimited precision";
17922            // we surface it as precision=0 to mean "unconstrained" so
17923            // the engine doesn't need a separate variant.
17924            return Ok((0, 0));
17925        }
17926        self.advance();
17927        // v7.39 (round 272) — PG's declared precision runs to 1000, and
17928        // it words the out-of-range case with the value it saw. SPG
17929        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17930        // accepts failed to parse at all; values wider than i128 are
17931        // carried by the arbitrary-precision form.
17932        let precision = match self.advance() {
17933            Token::Integer(n) if (1..=1000).contains(&n) => {
17934                u16::try_from(n).expect("range-checked")
17935            }
17936            Token::Integer(n) => {
17937                return Err(ParseError {
17938                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17939                    token_pos: self.consumed_pos(),
17940                });
17941            }
17942            other => {
17943                return Err(ParseError {
17944                    message: format!(
17945                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17946                    ),
17947                    token_pos: self.consumed_pos(),
17948                });
17949            }
17950        };
17951        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17952        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17953        // then overflows). A negative scale rounds to tens / hundreds / …
17954        let scale = if matches!(self.peek(), Token::Comma) {
17955            self.advance();
17956            let neg = if matches!(self.peek(), Token::Minus) {
17957                self.advance();
17958                true
17959            } else {
17960                false
17961            };
17962            match self.advance() {
17963                Token::Integer(n) => {
17964                    let signed = if neg { -n } else { n };
17965                    if !(-1000..=1000).contains(&signed) {
17966                        return Err(ParseError {
17967                            message: format!(
17968                                "NUMERIC scale {signed} must be between -1000 and 1000"
17969                            ),
17970                            token_pos: self.consumed_pos(),
17971                        });
17972                    }
17973                    i16::try_from(signed).expect("range-checked")
17974                }
17975                other => {
17976                    return Err(ParseError {
17977                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
17978                        token_pos: self.consumed_pos(),
17979                    });
17980                }
17981            }
17982        } else {
17983            0
17984        };
17985        if !matches!(self.peek(), Token::RParen) {
17986            return Err(self.err(format!(
17987                "expected ')' to close NUMERIC params, got {:?}",
17988                self.peek()
17989            )));
17990        }
17991        self.advance();
17992        Ok((precision, scale))
17993    }
17994
17995    /// Parse `(N)` where `N` is a positive integer literal — used by the
17996    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17997    /// for the error message.
17998    /// v6.0.1: parse the optional `USING <encoding>` clause that
17999    /// follows `VECTOR(N)` in a column definition. Missing clause
18000    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
18001    /// ident → `ParseError` listing the encodings recognised today.
18002    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
18003        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
18004            return Ok(VecEncoding::F32);
18005        }
18006        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
18007        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
18008        // consume the token when the very next token is a known
18009        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
18010        // `USING` for the caller — it's the rewrite-expression form.
18011        let n1 = self.tokens.get(self.pos + 1);
18012        let next_is_encoding = matches!(
18013            n1,
18014            Some(Token::Ident(s))
18015                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
18016        );
18017        if !next_is_encoding {
18018            return Ok(VecEncoding::F32);
18019        }
18020        self.advance();
18021        let enc_ident = match self.advance() {
18022            Token::Ident(s) => s,
18023            other => {
18024                return Err(self.err(format!(
18025                    "expected vector encoding after USING, got {other:?}"
18026                )));
18027            }
18028        };
18029        match enc_ident.to_ascii_lowercase().as_str() {
18030            "sq8" => Ok(VecEncoding::Sq8),
18031            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
18032            // binary16 per-element storage.
18033            "half" => Ok(VecEncoding::F16),
18034            other => Err(self.err(format!(
18035                "unknown vector encoding {other:?}; supported: SQ8, HALF"
18036            ))),
18037        }
18038    }
18039
18040    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
18041    /// without consuming it. Returns `Some(N)` when the next
18042    /// tokens are `( <int> )`; None otherwise. Used by the
18043    /// TINYINT classifier to decide whether to map to Bool or
18044    /// SmallInt.
18045    fn peek_optional_paren_size_value(&self) -> Option<i64> {
18046        if !matches!(self.peek(), Token::LParen) {
18047            return None;
18048        }
18049        let next = self.tokens.get(self.pos + 1)?;
18050        let n = match next {
18051            Token::Integer(n) => *n,
18052            _ => return None,
18053        };
18054        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18055            return None;
18056        }
18057        Some(n)
18058    }
18059
18060    /// v7.14.0 — consume an optional MySQL display-width
18061    /// parenthesised number after an integer type, returning
18062    /// nothing. `TINYINT(1)` etc.
18063    /// v7.39 (round 360) — does the parenthesised group ahead contain a
18064    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
18065    fn peek_paren_has_comma(&self) -> bool {
18066        let mut i = self.pos + 1;
18067        let mut depth = 1usize;
18068        while depth > 0 {
18069            match self.tokens.get(i) {
18070                Some(Token::LParen) => depth += 1,
18071                Some(Token::RParen) => depth -= 1,
18072                Some(Token::Comma) if depth == 1 => return true,
18073                None | Some(Token::Eof) => return false,
18074                _ => {}
18075            }
18076            i += 1;
18077        }
18078        false
18079    }
18080
18081    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
18082    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
18083    /// fractional-seconds precision that drives write truncation and render
18084    /// padding, where `consume_optional_paren_size` throws it away.
18085    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
18086    fn take_optional_paren_size(&mut self) -> Option<u8> {
18087        let Some(Token::Integer(n)) = self
18088            .tokens
18089            .get(self.pos + 1)
18090            .filter(|_| matches!(self.peek(), Token::LParen))
18091            .cloned()
18092        else {
18093            self.consume_optional_paren_size();
18094            return None;
18095        };
18096        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18097            self.consume_optional_paren_size();
18098            return None;
18099        }
18100        self.consume_optional_paren_size();
18101        u8::try_from(n).ok()
18102    }
18103
18104    fn consume_optional_paren_size(&mut self) {
18105        if !matches!(self.peek(), Token::LParen) {
18106            return;
18107        }
18108        self.advance();
18109        // Skip until matching RParen (allow nested or any tokens).
18110        let mut depth = 1usize;
18111        while depth > 0 {
18112            match self.peek() {
18113                Token::LParen => depth += 1,
18114                Token::RParen => depth -= 1,
18115                Token::Eof => return,
18116                _ => {}
18117            }
18118            self.advance();
18119        }
18120    }
18121
18122    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
18123        if !matches!(self.peek(), Token::LParen) {
18124            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
18125        }
18126        self.advance();
18127        let n = match self.advance() {
18128            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
18129                message: format!("{label} size too large: {n}"),
18130                token_pos: self.consumed_pos(),
18131            })?,
18132            other => {
18133                return Err(ParseError {
18134                    message: format!("expected positive integer {label} size, got {other:?}"),
18135                    token_pos: self.consumed_pos(),
18136                });
18137            }
18138        };
18139        if !matches!(self.peek(), Token::RParen) {
18140            return Err(self.err(format!(
18141                "expected ')' after {label} size, got {:?}",
18142                self.peek()
18143            )));
18144        }
18145        self.advance();
18146        Ok(n)
18147    }
18148
18149    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
18150    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
18151    /// key, like MySQL) whose action skips conflicting rows.
18152    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
18153    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
18154    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
18155    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
18156    /// common bulk-upsert spellings —
18157    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
18158    ///     REPLACE INTO t SELECT …
18159    /// — were a parse error / a duplicate-key failure respectively.
18160    ///
18161    /// Precedence: an explicitly written clause beats a statement-level flag.
18162    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
18163    /// implicit `REPLACE` and `IGNORE` lowerings.
18164    fn parse_insert_conflict_clause(
18165        &mut self,
18166        replace: bool,
18167        ignore: bool,
18168    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18169        if let Some(c) = self.parse_optional_on_duplicate_key()? {
18170            return Ok(Some(c));
18171        }
18172        if let Some(c) = self.parse_optional_on_conflict()? {
18173            return Ok(Some(c));
18174        }
18175        if replace {
18176            // REPLACE INTO = delete-then-insert, which PG spells as
18177            // `ON CONFLICT DO UPDATE SET` over every column; the engine
18178            // reads an empty assignment list as "take the incoming row".
18179            return Ok(Some(crate::ast::OnConflictClause {
18180                target_columns: Vec::new(),
18181                index_where: None,
18182                constraint_name: None,
18183                mysql_lowered: true,
18184                action: crate::ast::OnConflictAction::Update {
18185                    assignments: Vec::new(),
18186                    where_: None,
18187                },
18188            }));
18189        }
18190        if ignore {
18191            return Ok(Some(Self::insert_ignore_clause()));
18192        }
18193        Ok(None)
18194    }
18195
18196    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
18197    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
18198    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
18199    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
18200    fn parse_optional_on_duplicate_key(
18201        &mut self,
18202    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18203        if !(matches!(self.peek(), Token::On)
18204            && matches!(self.tokens.get(self.pos + 1),
18205                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
18206        {
18207            return Ok(None);
18208        }
18209        self.advance(); // ON
18210        self.advance(); // DUPLICATE
18211        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
18212            return Err(self.err(format!(
18213                "expected KEY after ON DUPLICATE, got {:?}",
18214                self.peek()
18215            )));
18216        }
18217        self.advance();
18218        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
18219            return Err(self.err(format!(
18220                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
18221                self.peek()
18222            )));
18223        }
18224        self.advance();
18225        let mut assignments: Vec<(String, Expr)> = Vec::new();
18226        loop {
18227            let col = self.expect_ident_like()?;
18228            if !matches!(self.peek(), Token::Eq) {
18229                return Err(self.err(format!(
18230                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
18231                    self.peek()
18232                )));
18233            }
18234            self.advance();
18235            let mut expr = self.parse_expr(0)?;
18236            Self::rewrite_mysql_values_refs(&mut expr);
18237            assignments.push((col, expr));
18238            if matches!(self.peek(), Token::Comma) {
18239                self.advance();
18240                continue;
18241            }
18242            break;
18243        }
18244        Ok(Some(crate::ast::OnConflictClause {
18245            target_columns: Vec::new(),
18246            index_where: None,
18247            constraint_name: None,
18248            mysql_lowered: true,
18249            action: crate::ast::OnConflictAction::Update {
18250                assignments,
18251                where_: None,
18252            },
18253        }))
18254    }
18255
18256    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
18257        crate::ast::OnConflictClause {
18258            target_columns: Vec::new(),
18259            index_where: None,
18260            constraint_name: None,
18261            mysql_lowered: true,
18262            action: crate::ast::OnConflictAction::Nothing,
18263        }
18264    }
18265
18266    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
18267        debug_assert!(
18268            matches!(self.peek(), Token::Insert)
18269                || (replace
18270                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
18271        );
18272        self.advance();
18273        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
18274        // would raise a duplicate-key error instead of failing the statement,
18275        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
18276        // plain ident to the lexer; only the MySQL dialect accepts it here.
18277        let ignore = self.mysql_dialect
18278            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
18279        if ignore {
18280            self.advance();
18281        }
18282        if !matches!(self.peek(), Token::Into) {
18283            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
18284        }
18285        self.advance();
18286        let table = self.expect_ident_like()?;
18287        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
18288        // grammar requires the AS keyword here (a bare identifier would be
18289        // ambiguous with a column list). The alias is what the ON CONFLICT
18290        // DO UPDATE expressions refer to the target row by.
18291        let alias = if matches!(self.peek(), Token::As) {
18292            self.advance();
18293            Some(self.expect_ident_like()?)
18294        } else {
18295            None
18296        };
18297        // v7.39 (round 428) — MySQL's SET-form INSERT:
18298        //     INSERT INTO t SET a = 1, b = 'x'
18299        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18300        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18301        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18302        // measured). So it lowers to the column list + one VALUES row and
18303        // rejoins the ordinary path, which already handles every one of
18304        // those. PG has no such spelling, hence the dialect gate.
18305        if self.mysql_dialect
18306            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18307        {
18308            self.advance(); // SET
18309            let mut names = Vec::new();
18310            let mut values = Vec::new();
18311            loop {
18312                names.push(self.expect_ident_like()?);
18313                if !matches!(self.peek(), Token::Eq) {
18314                    return Err(self.err(alloc::format!(
18315                        "expected '=' in INSERT … SET, got {:?}",
18316                        self.peek()
18317                    )));
18318                }
18319                self.advance();
18320                // `SET a = DEFAULT` rides the same `__column_default` marker
18321                // the VALUES-row and UPDATE-SET paths use; the INSERT
18322                // executor resolves it against the target column.
18323                if matches!(self.peek(), Token::Default) {
18324                    self.advance();
18325                    values.push(Expr::FunctionCall {
18326                        name: "__column_default".to_string(),
18327                        args: Vec::new(),
18328                    });
18329                } else {
18330                    values.push(self.parse_expr(0)?);
18331                }
18332                if matches!(self.peek(), Token::Comma) {
18333                    self.advance();
18334                    continue;
18335                }
18336                break;
18337            }
18338            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18339            let returning = self.parse_optional_returning()?;
18340            return Ok(Statement::Insert(InsertStatement {
18341                ctes: Vec::new(),
18342                table,
18343                alias,
18344                columns: Some(names),
18345                rows: alloc::vec![values],
18346                select_source: None,
18347                // MySQL's SET form has no `OVERRIDING …` clause (that is
18348                // PG's identity-column spelling).
18349                overriding: Overriding::None,
18350                mysql_ignore: ignore,
18351                on_conflict,
18352                returning,
18353            }));
18354        }
18355        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18356        // v7.39 (round 151) — a SELECT or WITH right after the paren is
18357        // a parenthesized query source instead (PG select_with_parens:
18358        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18359        // both keywords are reserved in PG, so no column list can start
18360        // with them.
18361        let columns = if matches!(self.peek(), Token::LParen) {
18362            self.advance();
18363            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18364                let select_stmt = if self.peek_is_with_kw() {
18365                    self.advance();
18366                    self.parse_nested_with_select()?
18367                } else {
18368                    match self.parse_select_stmt()? {
18369                        Statement::Select(s) => s,
18370                        other => {
18371                            return Err(self.err(alloc::format!(
18372                                "expected SELECT in parenthesized INSERT source, got {other:?}"
18373                            )));
18374                        }
18375                    }
18376                };
18377                if !matches!(self.peek(), Token::RParen) {
18378                    return Err(self.err(format!(
18379                        "expected ')' after parenthesized INSERT source, got {:?}",
18380                        self.peek()
18381                    )));
18382                }
18383                self.advance();
18384                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18385                let returning = self.parse_optional_returning()?;
18386                return Ok(Statement::Insert(InsertStatement {
18387                    ctes: Vec::new(),
18388                    table,
18389                    alias: alias.clone(),
18390                    columns: None,
18391                    rows: Vec::new(),
18392                    select_source: Some(Box::new(select_stmt)),
18393                    on_conflict,
18394                    returning,
18395                    overriding: Overriding::None,
18396                    mysql_ignore: ignore,
18397                }));
18398            }
18399            let mut names = Vec::new();
18400            loop {
18401                names.push(self.expect_ident_like()?);
18402                match self.peek() {
18403                    Token::Comma => {
18404                        self.advance();
18405                    }
18406                    Token::RParen => {
18407                        self.advance();
18408                        break;
18409                    }
18410                    other => {
18411                        return Err(self.err(format!(
18412                            "expected ',' or ')' in INSERT column list, got {other:?}"
18413                        )));
18414                    }
18415                }
18416            }
18417            Some(names)
18418        } else {
18419            None
18420        };
18421        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18422        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18423        // is captured on the statement so the engine can apply PG's
18424        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18425        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18426        {
18427            self.advance();
18428            let which = self.expect_ident_like()?;
18429            let ov = if which.eq_ignore_ascii_case("system") {
18430                Overriding::System
18431            } else if which.eq_ignore_ascii_case("user") {
18432                Overriding::User
18433            } else {
18434                return Err(self.err(format!(
18435                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18436                )));
18437            };
18438            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18439                return Err(self.err(format!(
18440                    "expected VALUE after OVERRIDING {}, got {:?}",
18441                    which.to_ascii_uppercase(),
18442                    self.peek()
18443                )));
18444            }
18445            self.advance();
18446            ov
18447        } else {
18448            Overriding::None
18449        };
18450        // `INSERT INTO t DEFAULT VALUES` — a single row made
18451        // entirely of column defaults. Lower to the permuted
18452        // column-list path with an empty list: every schema column
18453        // is unmapped, so the engine fills each from its default
18454        // (serials advance, plain defaults evaluate, the rest NULL).
18455        if matches!(self.peek(), Token::Default) {
18456            self.advance();
18457            if !matches!(self.peek(), Token::Values) {
18458                return Err(self.err(format!(
18459                    "expected VALUES after DEFAULT in INSERT, got {:?}",
18460                    self.peek()
18461                )));
18462            }
18463            self.advance();
18464            if columns.is_some() {
18465                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18466            }
18467            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18468            let returning = self.parse_optional_returning()?;
18469            return Ok(Statement::Insert(InsertStatement {
18470                ctes: Vec::new(),
18471                table,
18472                alias: alias.clone(),
18473                columns: Some(Vec::new()),
18474                rows: alloc::vec![Vec::new()],
18475                select_source: None,
18476                on_conflict,
18477                returning,
18478                overriding,
18479                mysql_ignore: ignore,
18480            }));
18481        }
18482        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18483        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18484        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18485        // SELECT …`) heads the SOURCE select, as in PG (the statement's
18486        // own WITH comes before INSERT).
18487        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18488            let select_stmt = if self.peek_is_with_kw() {
18489                self.advance();
18490                self.parse_nested_with_select()?
18491            } else {
18492                match self.parse_select_stmt()? {
18493                    Statement::Select(s) => s,
18494                    other => {
18495                        return Err(self.err(alloc::format!(
18496                            "expected SELECT after INSERT INTO ... target, got {other:?}"
18497                        )));
18498                    }
18499                }
18500            };
18501            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18502            let returning = self.parse_optional_returning()?;
18503            return Ok(Statement::Insert(InsertStatement {
18504                ctes: Vec::new(),
18505                table,
18506                alias: alias.clone(),
18507                columns,
18508                rows: Vec::new(),
18509                select_source: Some(Box::new(select_stmt)),
18510                on_conflict,
18511                returning,
18512                overriding,
18513                mysql_ignore: ignore,
18514            }));
18515        }
18516        if !matches!(self.peek(), Token::Values) {
18517            return Err(self.err(format!(
18518                "expected VALUES or SELECT after table name, got {:?}",
18519                self.peek()
18520            )));
18521        }
18522        self.advance();
18523        if !matches!(self.peek(), Token::LParen) {
18524            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18525        }
18526        let mut rows = Vec::new();
18527        loop {
18528            // Each iteration consumes one `(expr, expr, …)` tuple.
18529            if !matches!(self.peek(), Token::LParen) {
18530                return Err(self.err(format!(
18531                    "expected '(' for next VALUES tuple, got {:?}",
18532                    self.peek()
18533                )));
18534            }
18535            self.advance();
18536            let mut tuple = Vec::new();
18537            loop {
18538                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18539                // the column's declared default for that slot. Rides out as the
18540                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18541                // path uses; the INSERT executor resolves it per target column.
18542                if matches!(self.peek(), Token::Default) {
18543                    self.advance();
18544                    tuple.push(Expr::FunctionCall {
18545                        name: "__column_default".to_string(),
18546                        args: Vec::new(),
18547                    });
18548                } else {
18549                    tuple.push(self.parse_expr(0)?);
18550                }
18551                match self.peek() {
18552                    Token::Comma => {
18553                        self.advance();
18554                    }
18555                    Token::RParen => {
18556                        self.advance();
18557                        break;
18558                    }
18559                    other => {
18560                        return Err(self.err(format!(
18561                            "expected ',' or ')' in VALUES tuple, got {other:?}"
18562                        )));
18563                    }
18564                }
18565            }
18566            if tuple.is_empty() {
18567                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18568            }
18569            rows.push(tuple);
18570            // Continue with comma-separated tuples.
18571            if matches!(self.peek(), Token::Comma) {
18572                self.advance();
18573            } else {
18574                break;
18575            }
18576        }
18577        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18578        // to ON CONFLICT DO UPDATE with an empty conflict target
18579        // (the engine picks the table's first unique index, which
18580        // matches MySQL's any-unique-key behaviour for the common
18581        // single-key case). `VALUES(col)` in the assignments is
18582        // MySQL's spelling of EXCLUDED.col.
18583        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18584        let returning = self.parse_optional_returning()?;
18585        Ok(Statement::Insert(InsertStatement {
18586            ctes: Vec::new(),
18587            table,
18588            alias,
18589            columns,
18590            rows,
18591            select_source: None,
18592            on_conflict,
18593            returning,
18594            overriding,
18595            mysql_ignore: ignore,
18596        }))
18597    }
18598
18599    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18600    /// the incoming row's value — exactly PG's EXCLUDED.col.
18601    fn rewrite_mysql_values_refs(e: &mut Expr) {
18602        match e {
18603            Expr::FunctionCall { name, args }
18604                if name.eq_ignore_ascii_case("values")
18605                    && args.len() == 1
18606                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18607            {
18608                let Expr::Column(c) = &args[0] else {
18609                    unreachable!("guarded above");
18610                };
18611                *e = Expr::Column(crate::ast::ColumnName {
18612                    qualifier: Some("EXCLUDED".to_string()),
18613                    name: c.name.clone(),
18614                });
18615            }
18616            Expr::FunctionCall { args, .. } => {
18617                for a in args {
18618                    Self::rewrite_mysql_values_refs(a);
18619                }
18620            }
18621            Expr::Binary { lhs, rhs, .. } => {
18622                Self::rewrite_mysql_values_refs(lhs);
18623                Self::rewrite_mysql_values_refs(rhs);
18624            }
18625            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18626                Self::rewrite_mysql_values_refs(expr);
18627            }
18628            Expr::Case {
18629                operand,
18630                branches,
18631                else_branch,
18632            } => {
18633                if let Some(op) = operand {
18634                    Self::rewrite_mysql_values_refs(op);
18635                }
18636                for (w, t) in branches {
18637                    Self::rewrite_mysql_values_refs(w);
18638                    Self::rewrite_mysql_values_refs(t);
18639                }
18640                if let Some(el) = else_branch {
18641                    Self::rewrite_mysql_values_refs(el);
18642                }
18643            }
18644            _ => {}
18645        }
18646    }
18647
18648    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18649    /// clause sitting between the INSERT body and the trailing
18650    /// RETURNING. All keywords come in as bare idents; `ON` is
18651    /// a reserved Token though.
18652    fn parse_optional_on_conflict(
18653        &mut self,
18654    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18655        if !matches!(self.peek(), Token::On) {
18656            return Ok(None);
18657        }
18658        // Peek further: we want exactly "ON CONFLICT ...". If the
18659        // next ident isn't "conflict", let some other parser handle.
18660        let next_is_conflict = matches!(
18661            self.tokens.get(self.pos + 1),
18662            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18663        );
18664        if !next_is_conflict {
18665            return Ok(None);
18666        }
18667        self.advance(); // ON
18668        self.advance(); // CONFLICT
18669        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18670        // the constraint instead of listing columns (the pg_dump
18671        // form); the engine resolves it.
18672        let mut constraint_name: Option<String> = None;
18673        if matches!(self.peek(), Token::On) {
18674            self.advance(); // ON
18675            match self.advance() {
18676                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18677                }
18678                other => {
18679                    return Err(self.err(alloc::format!(
18680                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18681                    )));
18682                }
18683            }
18684            constraint_name = Some(self.expect_ident_like()?);
18685        }
18686        // Optional `(col [, col]*)` target list.
18687        let mut target_columns: Vec<String> = Vec::new();
18688        if matches!(self.peek(), Token::LParen) {
18689            self.advance();
18690            loop {
18691                target_columns.push(self.expect_ident_like()?);
18692                match self.peek() {
18693                    Token::Comma => {
18694                        self.advance();
18695                    }
18696                    Token::RParen => {
18697                        self.advance();
18698                        break;
18699                    }
18700                    other => {
18701                        return Err(self.err(alloc::format!(
18702                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18703                        )));
18704                    }
18705                }
18706            }
18707        }
18708        // v7.39 (round 240) — optional index predicate after the target
18709        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18710        // PARTIAL unique index; SPG's arbiters are full indexes, which
18711        // satisfy any predicate, so it is parsed and carried but not
18712        // consulted (recorded residual: partial-unique-index arbiters).
18713        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18714            self.advance();
18715            Some(self.parse_expr(0)?)
18716        } else {
18717            None
18718        };
18719        // Required `DO`.
18720        match self.advance() {
18721            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18722            other => {
18723                return Err(self.err(alloc::format!(
18724                    "expected DO after ON CONFLICT [(…)], got {other:?}"
18725                )));
18726            }
18727        }
18728        // Action: NOTHING | UPDATE SET …
18729        let action = match self.advance() {
18730            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18731                crate::ast::OnConflictAction::Nothing
18732            }
18733            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18734                self.parse_on_conflict_update_action()?
18735            }
18736            other => {
18737                return Err(self.err(alloc::format!(
18738                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18739                )));
18740            }
18741        };
18742        Ok(Some(crate::ast::OnConflictClause {
18743            target_columns,
18744            index_where,
18745            constraint_name,
18746            mysql_lowered: false,
18747            action,
18748        }))
18749    }
18750
18751    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18752    /// `SET col = expr [, …] [WHERE cond]`. Caller already
18753    /// consumed `UPDATE`.
18754    fn parse_on_conflict_update_action(
18755        &mut self,
18756    ) -> Result<crate::ast::OnConflictAction, ParseError> {
18757        // `SET`
18758        match self.advance() {
18759            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18760            other => {
18761                return Err(self.err(alloc::format!(
18762                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18763                )));
18764            }
18765        }
18766        let mut assignments: Vec<(String, Expr)> = Vec::new();
18767        loop {
18768            let col = self.expect_ident_like()?;
18769            if !matches!(self.peek(), Token::Eq) {
18770                return Err(self.err(alloc::format!(
18771                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18772                    self.peek()
18773                )));
18774            }
18775            self.advance();
18776            let value = self.parse_expr(0)?;
18777            assignments.push((col, value));
18778            if matches!(self.peek(), Token::Comma) {
18779                self.advance();
18780                continue;
18781            }
18782            break;
18783        }
18784        let where_ = if matches!(self.peek(), Token::Where) {
18785            self.advance();
18786            Some(self.parse_expr(0)?)
18787        } else {
18788            None
18789        };
18790        Ok(crate::ast::OnConflictAction::Update {
18791            assignments,
18792            where_,
18793        })
18794    }
18795
18796    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18797        let mut items = Vec::new();
18798        // v7.39 (round 341, V66) — PG's target list may be EMPTY
18799        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18800        // answers one zero-column row per row of t, and a bare `SELECT`
18801        // answers a single zero-column row. SPG required at least one
18802        // item, so both were syntax errors. Recognised by the token that
18803        // follows — nothing that can start an expression appears here.
18804        if self.select_list_is_empty_here() {
18805            return Ok(items);
18806        }
18807        loop {
18808            items.push(self.parse_select_item()?);
18809            if matches!(self.peek(), Token::Comma) {
18810                self.advance();
18811            } else {
18812                break;
18813            }
18814        }
18815        Ok(items)
18816    }
18817
18818    /// Is the target list empty at this point — i.e. does the next token
18819    /// end the SELECT's item list rather than start an item?
18820    fn select_list_is_empty_here(&self) -> bool {
18821        match self.peek() {
18822            Token::From
18823            | Token::Where
18824            | Token::Group
18825            | Token::Having
18826            | Token::Order
18827            | Token::Limit
18828            | Token::Offset
18829            | Token::Semicolon
18830            | Token::RParen
18831            | Token::Union
18832            | Token::Except
18833            | Token::Eof => true,
18834            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18835            // with unreserved keywords, so they arrive as plain idents.
18836            Token::Ident(s) => {
18837                s.eq_ignore_ascii_case("fetch")
18838                    || s.eq_ignore_ascii_case("window")
18839                    || s.eq_ignore_ascii_case("intersect")
18840            }
18841            _ => false,
18842        }
18843    }
18844
18845    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18846        if matches!(self.peek(), Token::Star) {
18847            self.advance();
18848            return Ok(SelectItem::Wildcard);
18849        }
18850        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18851        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18852        // choke on the `*` ("expected identifier, got Star"). The lookahead is
18853        // `<ident> . *` with nothing binding tighter.
18854        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18855            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18856                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18857            {
18858                self.advance(); // qualifier
18859                self.advance(); // .
18860                self.advance(); // *
18861                return Ok(SelectItem::QualifiedWildcard(q));
18862            }
18863        }
18864        let start_tok = self.pos;
18865        let expr = self.parse_expr(0)?;
18866        let end_tok = self.consumed_pos();
18867        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18868        // multi-column function returns into columns. Marked here and lowered in
18869        // `parse_bare_select`, where the FROM clause is in hand.
18870        if matches!(self.peek(), Token::Dot)
18871            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18872        {
18873            self.advance(); // .
18874            self.advance(); // *
18875            return Ok(SelectItem::Expr {
18876                expr: Expr::FunctionCall {
18877                    name: "__record_expand".to_string(),
18878                    args: alloc::vec![expr],
18879                },
18880                alias: None,
18881            });
18882        }
18883        // v7.39.2 — MySQL lets a STRING name a projection item, with or
18884        // without `AS`: `SELECT 1 'x'`, `SELECT COUNT(*) 'total'`,
18885        // `SELECT 1 'a b'` (which is why one quotes it). SPG answered
18886        // `syntax error at or near "'x'"` to all of them.
18887        //
18888        // Only here, not in `parse_optional_alias`: that one also names
18889        // TABLES, and MySQL 9.7.2 refuses a string there — `FROM t 'ta'`
18890        // and `FROM t AS 'ta'` are both syntax errors, measured. And only
18891        // after the lexer's own rule has joined adjacent literals, or
18892        // `SELECT 'a' 'b'` would read as a literal aliased `b` where
18893        // MySQL answers the concatenation `ab`.
18894        if self.mysql_dialect {
18895            let at_as = matches!(self.peek(), Token::As)
18896                && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)));
18897            if at_as {
18898                self.advance();
18899            }
18900            if let Token::String(name) = self.peek().clone() {
18901                self.advance();
18902                return Ok(SelectItem::Expr {
18903                    expr,
18904                    alias: Some(name),
18905                });
18906            }
18907        }
18908        let alias = match self.parse_optional_alias()? {
18909            Some(a) => Some(a),
18910            None => self.mysql_item_label(&expr, start_tok, end_tok),
18911        };
18912        Ok(SelectItem::Expr { expr, alias })
18913    }
18914
18915    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18916    /// carries no `AS`, filled in here so every downstream path reports it
18917    /// without knowing the rule. `None` leaves the item un-aliased, which is
18918    /// what a PG session always gets.
18919    ///
18920    /// Measured against MariaDB 11, three rules and no more:
18921    ///
18922    /// | item             | label      | why                          |
18923    /// |------------------|------------|------------------------------|
18924    /// | `lbl.a`          | `a`        | a column reports its name    |
18925    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
18926    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
18927    ///
18928    /// The third is why this lives in the parser at all: the label is the
18929    /// text the client WROTE, down to the spacing, so it cannot be printed
18930    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18931    ///
18932    /// Comments survive, and that is right: through a `mariadb` CLI both
18933    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
18934    /// CLIENT stripping the comment before it sends. Asked over the raw
18935    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18936    /// produces.
18937    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18938        if !self.mysql_dialect {
18939            return None;
18940        }
18941        match expr {
18942            // A column already reports its own name downstream; naming it
18943            // again here would only re-state the qualifier the label drops.
18944            Expr::Column(_) => None,
18945            // v7.39.3 — `SELECT 'a' 'b'` is ONE literal whose value is
18946            // `ab`, and MySQL 9.7.2 names the column `a`: the label is
18947            // the first segment as written, not the joined value
18948            // (measured). The lexer logs where it joined them.
18949            Expr::Literal(Literal::String(v)) => Some(
18950                self.merged_first_len(start_tok)
18951                    .and_then(|n| v.get(..n))
18952                    .map_or_else(|| v.clone(), String::from),
18953            ),
18954            _ => self.source_span(start_tok, end_tok).map(str::to_string),
18955        }
18956    }
18957
18958    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18959    /// consumed VALUES keyword. Each row lowers to a constant SELECT
18960    /// with PG's default column1..columnN names; subsequent rows
18961    /// chain as UNION ALL peers. Shared by the FROM-position
18962    /// `( VALUES … )` arm and the top-level bare VALUES statement.
18963    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18964        let mut row_selects: Vec<SelectStatement> = Vec::new();
18965        loop {
18966            if !matches!(self.peek(), Token::LParen) {
18967                return Err(self.err(alloc::format!(
18968                    "expected '(' to start a VALUES row, got {:?}",
18969                    self.peek()
18970                )));
18971            }
18972            self.advance(); // (
18973            let mut items: Vec<SelectItem> = Vec::new();
18974            loop {
18975                let expr = self.parse_expr(0)?;
18976                items.push(SelectItem::Expr {
18977                    expr,
18978                    alias: Some(alloc::format!("column{}", items.len() + 1)),
18979                });
18980                match self.peek() {
18981                    Token::Comma => {
18982                        self.advance();
18983                    }
18984                    Token::RParen => break,
18985                    other => {
18986                        return Err(self.err(alloc::format!(
18987                            "expected ',' or ')' in VALUES row, got {other:?}"
18988                        )));
18989                    }
18990                }
18991            }
18992            self.advance(); // )
18993            row_selects.push(SelectStatement {
18994                locking: None,
18995                ctes: Vec::new(),
18996                distinct: false,
18997                distinct_on: Vec::new(),
18998                items,
18999                from: None,
19000                where_: None,
19001                group_by: None,
19002                group_by_all: false,
19003                having: None,
19004                unions: Vec::new(),
19005                order_by: Vec::new(),
19006                limit: None,
19007                offset: None,
19008                limit_with_ties: false,
19009                window_check_exprs: Vec::new(),
19010            });
19011            if matches!(self.peek(), Token::Comma) {
19012                self.advance();
19013                continue;
19014            }
19015            break;
19016        }
19017        let mut head = row_selects.remove(0);
19018        head.unions = row_selects
19019            .into_iter()
19020            .map(|s| (UnionKind::All, s))
19021            .collect();
19022        Ok(head)
19023    }
19024
19025    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
19026        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
19027        // children. It was read as a table NAMED `only`, so the query
19028        // failed on `relation "only" does not exist`.
19029        //
19030        // v7.39 (round 644) — and it is no longer a no-op. Round 621
19031        // absorbed the keyword, reasoning that SPG's children are
19032        // separate relations a plain scan does not descend into, so ONLY
19033        // already described the scan. That stopped being true when a
19034        // partition parent started unioning its children: measured,
19035        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
19036        // where PG answers 0. The flag is carried now.
19037        let mut only = false;
19038        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
19039            && matches!(
19040                self.tokens.get(self.pos + 1),
19041                Some(Token::Ident(_) | Token::QuotedIdent(_))
19042            )
19043        {
19044            only = true;
19045            self.advance();
19046        }
19047        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
19048        // for these SRFs the keyword is noise at parse time: the
19049        // join executor already substitutes outer-column references
19050        // into unnest_expr / generate_series_args per outer row
19051        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
19052        // licences the correlation even without the keyword. Absorb
19053        // it and fall through to the SRF arms below.
19054        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
19055        // just the four builtin SRFs: a user set-returning function on a JOIN's
19056        // right side is the whole point of LATERAL. The keyword stays noise at
19057        // parse time — the join executor substitutes the outer row into the
19058        // call's arguments per outer row.
19059        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19060            && matches!(
19061                self.tokens.get(self.pos + 1),
19062                // The json_each family has its OWN `LATERAL …` arm below, which
19063                // needs to see the keyword — absorbing it here would send those
19064                // calls down the generic table-function channel instead.
19065                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
19066            )
19067            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19068        {
19069            self.advance(); // LATERAL
19070        }
19071        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
19072        // set-returning function whose argument may reference a
19073        // preceding FROM item. We rewrite this to
19074        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
19075        // AS __srf__) AS <alias>` so the existing LATERAL subquery
19076        // executor handles per-outer-row evaluation and the
19077        // SRF-primary jsonb_each_text path handles the inner
19078        // materialisation. Sentori 0067 backfill is the dogfood
19079        // shape.
19080        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19081            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
19082            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19083        {
19084            self.advance(); // LATERAL
19085            let each_fn = match self.peek() {
19086                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19087                _ => unreachable!(),
19088            };
19089            self.advance(); // jsonb_each[_text] / json_each[_text]
19090            self.advance(); // (
19091            let arg = self.parse_expr(0)?;
19092            if !matches!(self.peek(), Token::RParen) {
19093                return Err(self.err(alloc::format!(
19094                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
19095                    self.peek()
19096                )));
19097            }
19098            self.advance();
19099            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19100            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19101            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
19102            //               FROM jsonb_each_text(<arg>) AS __srf__
19103            // PG's `AS kv(key, value)` column-alias list maps
19104            // positions to names; default to (key, value) when
19105            // omitted (matching the SRF's natural column names).
19106            let srf_alias = "__srf__".to_string();
19107            let key_alias = column_aliases
19108                .first()
19109                .cloned()
19110                .unwrap_or_else(|| "key".to_string());
19111            let value_alias = column_aliases
19112                .get(1)
19113                .cloned()
19114                .unwrap_or_else(|| "value".to_string());
19115            let inner_select = crate::ast::SelectStatement {
19116                locking: None,
19117                ctes: Vec::new(),
19118                distinct: false,
19119                distinct_on: Vec::new(),
19120                items: alloc::vec![
19121                    crate::ast::SelectItem::Expr {
19122                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19123                            qualifier: Some(srf_alias.clone()),
19124                            name: "key".to_string(),
19125                        }),
19126                        alias: Some(key_alias),
19127                    },
19128                    crate::ast::SelectItem::Expr {
19129                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19130                            qualifier: Some(srf_alias.clone()),
19131                            name: "value".to_string(),
19132                        }),
19133                        alias: Some(value_alias),
19134                    },
19135                ],
19136                from: Some(crate::ast::FromClause {
19137                    primary: TableRef {
19138                        name: srf_alias.clone(),
19139                        alias: Some(srf_alias.clone()),
19140                        only: false,
19141                        as_of_segment: None,
19142                        unnest_expr: None,
19143                        unnest_column_aliases: Vec::new(),
19144                        with_ordinality: false,
19145                        generate_series_args: None,
19146                        lateral_subquery: None,
19147                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19148                        table_fn_call: None,
19149                        rows_from: None,
19150                        json_table: None,
19151                        scalar_fn_item: false,
19152                    },
19153                    joins: Vec::new(),
19154                }),
19155                where_: None,
19156                group_by: None,
19157                group_by_all: false,
19158                having: None,
19159                unions: Vec::new(),
19160                order_by: Vec::new(),
19161                limit: None,
19162                offset: None,
19163                limit_with_ties: false,
19164                window_check_exprs: Vec::new(),
19165            };
19166            return Ok(TableRef {
19167                name: alias.clone(),
19168                alias: Some(alias),
19169                only: false,
19170                as_of_segment: None,
19171                unnest_expr: None,
19172                unnest_column_aliases: Vec::new(),
19173                with_ordinality: false,
19174                generate_series_args: None,
19175                lateral_subquery: Some(Box::new(inner_select)),
19176                jsonb_each_text_arg: None,
19177                table_fn_call: None,
19178                rows_from: None,
19179                json_table: None,
19180                scalar_fn_item: false,
19181            });
19182        }
19183        // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
19184        // without an explicit `LATERAL` keyword is the same shape
19185        // PG accepts (SRF naturally licences lateral correlation).
19186        // We mirror the LATERAL rewrite when the argument syntactic-
19187        // ally references an outer column (Column { qualifier:
19188        // Some(_), … }). For simplicity we apply the rewrite
19189        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
19190        // in the FROM-list — caller-side join parsing positions
19191        // this peek correctly.
19192        // (Implementation note: detection lives below; the LATERAL
19193        // branch above already covers the explicit form; the bare
19194        // form falls through to the plain SRF arm and the engine
19195        // treats it as a constant-arg SRF if no outer reference is
19196        // present.)
19197        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
19198        // table. Detect at the head so it claims precedence over
19199        // every other table-ref shape (unnest / generate_series /
19200        // bare ident); the lateral subquery itself follows the
19201        // regular SELECT grammar.
19202        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
19203        // t(cols)`. Each row lowers to a constant SELECT with PG's
19204        // default column1..columnN names; subsequent rows chain as
19205        // UNION ALL peers. The result rides the derived-table
19206        // lateral_subquery channel — zero executor work.
19207        if matches!(self.peek(), Token::LParen)
19208            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
19209        {
19210            self.advance(); // (
19211            self.advance(); // VALUES
19212            let head = self.parse_values_rows_body()?;
19213            if !matches!(self.peek(), Token::RParen) {
19214                return Err(self.err(alloc::format!(
19215                    "expected ')' after VALUES list, got {:?}",
19216                    self.peek()
19217                )));
19218            }
19219            self.advance();
19220            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19221            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
19222            return Ok(TableRef {
19223                name,
19224                alias: alias_ident,
19225                only: false,
19226                as_of_segment: None,
19227                unnest_expr: None,
19228                unnest_column_aliases: column_aliases,
19229                with_ordinality: false,
19230                generate_series_args: None,
19231                lateral_subquery: Some(Box::new(head)),
19232                jsonb_each_text_arg: None,
19233                table_fn_call: None,
19234                rows_from: None,
19235                json_table: None,
19236                scalar_fn_item: false,
19237            });
19238        }
19239        // v7.37.17 (17.6 siblings) — plain derived table:
19240        // `FROM ( SELECT … ) [AS] alias`. Rides the same
19241        // lateral_subquery channel the explicit LATERAL form uses —
19242        // an uncorrelated inner SELECT executes identically. The
19243        // inner parse carries UNION tails (they live on
19244        // SelectStatement.unions).
19245        // v7.37 D.20 — the derived-table inner may itself be a
19246        // parenthesized set-operation group (`FROM ((SELECT…) UNION
19247        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
19248        // bare `(SELECT …)`. parse_one_statement already routes a leading
19249        // `(` set-op group (its LParen arm) and a leading WITH
19250        // (parse_with_cte_then_select), so widen the second-token gate to
19251        // Select | LParen | WITH.
19252        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
19253        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
19254        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
19255        // has existed since the shorthand landed and `parse_bare_select`
19256        // already routes it ("valid anywhere a SELECT head is"); what was
19257        // missing is this second-token gate, and the CTE body's dispatch
19258        // below. Round 868 found both by putting the shorthand in a
19259        // subquery — the top-level forms had been the only ones tested.
19260        if matches!(self.peek(), Token::LParen)
19261            && (matches!(
19262                self.tokens.get(self.pos + 1),
19263                Some(Token::Select | Token::LParen | Token::Table)
19264            ) || matches!(self.tokens.get(self.pos + 1),
19265                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
19266        {
19267            self.advance(); // (
19268            let inner = match self.parse_one_statement()? {
19269                Statement::Select(s) => s,
19270                other => {
19271                    return Err(self.err(alloc::format!(
19272                        "expected SELECT inside derived table ( … ), got {other:?}"
19273                    )));
19274                }
19275            };
19276            if !matches!(self.peek(), Token::RParen) {
19277                return Err(self.err(alloc::format!(
19278                    "expected ')' after derived-table subquery, got {:?}",
19279                    self.peek()
19280                )));
19281            }
19282            self.advance();
19283            // `AS t(a, b)` column-alias list rides the
19284            // unnest_column_aliases field (same positional-rename
19285            // contract the unnest SRFs use).
19286            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19287            let name = alias_ident
19288                .clone()
19289                .unwrap_or_else(|| "subquery".to_string());
19290            return Ok(TableRef {
19291                name,
19292                alias: alias_ident,
19293                only: false,
19294                as_of_segment: None,
19295                unnest_expr: None,
19296                unnest_column_aliases: column_aliases,
19297                with_ordinality: false,
19298                generate_series_args: None,
19299                lateral_subquery: Some(Box::new(inner)),
19300                jsonb_each_text_arg: None,
19301                table_fn_call: None,
19302                rows_from: None,
19303                json_table: None,
19304                scalar_fn_item: false,
19305            });
19306        }
19307        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19308            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19309        {
19310            self.advance(); // LATERAL
19311            self.advance(); // (
19312            // Parse the inner SELECT.
19313            let inner = match self.parse_one_statement()? {
19314                Statement::Select(s) => s,
19315                other => {
19316                    return Err(self.err(alloc::format!(
19317                        "expected SELECT inside LATERAL ( … ), got {other:?}"
19318                    )));
19319                }
19320            };
19321            if !matches!(self.peek(), Token::RParen) {
19322                return Err(self.err(alloc::format!(
19323                    "expected ')' after LATERAL subquery, got {:?}",
19324                    self.peek()
19325                )));
19326            }
19327            self.advance();
19328            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19329            // `(VALUES …) t(g)` derived table round-trips through view-body
19330            // Display, which renders on the lateral_subquery channel).
19331            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19332            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19333            return Ok(TableRef {
19334                name,
19335                alias: alias_ident,
19336                only: false,
19337                as_of_segment: None,
19338                unnest_expr: None,
19339                unnest_column_aliases: column_aliases,
19340                with_ordinality: false,
19341                generate_series_args: None,
19342                lateral_subquery: Some(Box::new(inner)),
19343                jsonb_each_text_arg: None,
19344                table_fn_call: None,
19345                rows_from: None,
19346                json_table: None,
19347                scalar_fn_item: false,
19348            });
19349        }
19350        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19351        // function as a FROM item. Emits one row per (key, value)
19352        // pair in the JSONB object argument as TEXT columns. May
19353        // be wrapped in CROSS JOIN LATERAL when the argument
19354        // references a preceding FROM item (sentori migration
19355        // 0067 backfill shape: `CROSS JOIN LATERAL
19356        // jsonb_each_text(t.json_col) AS kv(key, value)`).
19357        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19358            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19359        {
19360            let each_fn = match self.peek() {
19361                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19362                _ => unreachable!(),
19363            };
19364            self.advance(); // jsonb_each[_text] / json_each[_text]
19365            self.advance(); // (
19366            let arg = self.parse_expr(0)?;
19367            if !matches!(self.peek(), Token::RParen) {
19368                return Err(self.err(alloc::format!(
19369                    "expected ')' after {each_fn}() argument, got {:?}",
19370                    self.peek()
19371                )));
19372            }
19373            self.advance();
19374            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19375            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19376            return Ok(TableRef {
19377                name,
19378                alias: alias_ident,
19379                only: false,
19380                as_of_segment: None,
19381                unnest_expr: None,
19382                // `AS t(k, v)` renames key/value positionally, same as the
19383                // LATERAL-position form already does.
19384                unnest_column_aliases: column_aliases,
19385                with_ordinality: false,
19386                generate_series_args: None,
19387                lateral_subquery: None,
19388                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19389                table_fn_call: None,
19390                rows_from: None,
19391                json_table: None,
19392                scalar_fn_item: false,
19393            });
19394        }
19395        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19396        // (+ json_ variants) — record-returning JSON functions with a
19397        // column-definition list. Desugar to a derived table that
19398        // projects each declared column from the JSON via `->>` + a cast,
19399        // over `jsonb_array_elements(J)` for the *set (per-element) form.
19400        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19401            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19402        {
19403            return self.parse_json_to_record_from();
19404        }
19405        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19406        // row is a text[] of capture groups, so it cannot desugar to unnest
19407        // (that would flatten the array). Wrap it as a derived table
19408        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19409        // SRF path already emits one text[] row per match. PG names the column
19410        // `regexp_matches`; an `AS a(col)` alias overrides it.
19411        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19412                if s.eq_ignore_ascii_case("regexp_matches"))
19413            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19414        {
19415            self.advance(); // fn name
19416            self.advance(); // (
19417            let mut fn_args: Vec<Expr> = Vec::new();
19418            loop {
19419                fn_args.push(self.parse_expr(0)?);
19420                if matches!(self.peek(), Token::Comma) {
19421                    self.advance();
19422                    continue;
19423                }
19424                break;
19425            }
19426            if !matches!(self.peek(), Token::RParen) {
19427                return Err(self.err(alloc::format!(
19428                    "expected ')' after regexp_matches() arguments, got {:?}",
19429                    self.peek()
19430                )));
19431            }
19432            self.advance();
19433            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19434            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19435            // it, so it died on the `with` token while every other table function
19436            // accepted it.
19437            let with_ordinality = self.absorb_with_ordinality();
19438            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19439            let table_alias = alias_ident
19440                .clone()
19441                .unwrap_or_else(|| "regexp_matches".to_string());
19442            // PG names a single-column function's output column after the ALIAS
19443            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19444            // `m` reads as that column and not as a whole-row composite. Naming
19445            // it after the function regardless made `SELECT m[1] FROM … AS m`
19446            // subscript a record.
19447            let col_name = column_aliases
19448                .first()
19449                .cloned()
19450                .or_else(|| alias_ident.clone())
19451                .unwrap_or_else(|| "regexp_matches".to_string());
19452            let inner = crate::ast::SelectStatement {
19453                locking: None,
19454                ctes: Vec::new(),
19455                distinct: false,
19456                distinct_on: Vec::new(),
19457                items: alloc::vec![SelectItem::Expr {
19458                    expr: Expr::FunctionCall {
19459                        name: "regexp_matches".to_string(),
19460                        args: fn_args,
19461                    },
19462                    alias: Some(col_name),
19463                }],
19464                from: None,
19465                where_: None,
19466                group_by: None,
19467                group_by_all: false,
19468                having: None,
19469                unions: Vec::new(),
19470                order_by: Vec::new(),
19471                limit: None,
19472                offset: None,
19473                limit_with_ties: false,
19474                window_check_exprs: Vec::new(),
19475            };
19476            return Ok(TableRef {
19477                name: table_alias.clone(),
19478                alias: Some(table_alias),
19479                only: false,
19480                as_of_segment: None,
19481                unnest_expr: None,
19482                unnest_column_aliases: column_aliases,
19483                with_ordinality,
19484                generate_series_args: None,
19485                lateral_subquery: Some(Box::new(inner)),
19486                jsonb_each_text_arg: None,
19487                table_fn_call: None,
19488                rows_from: None,
19489                json_table: None,
19490                // regexp_matches returns text[], a base type: `SELECT m FROM
19491                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19492                scalar_fn_item: true,
19493            });
19494        }
19495        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19496        // / json_ variants as a FROM item. Rewritten into
19497        // `unnest(<same fn>(<expr>))`: the scalar form returns the
19498        // elements as a TEXT array, and the existing unnest SRF path
19499        // materialises one row per element. PG's natural column name
19500        // is `value`; an `AS a(col)` column-alias list overrides it.
19501        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19502                if s.eq_ignore_ascii_case("jsonb_array_elements")
19503                    || s.eq_ignore_ascii_case("json_array_elements")
19504                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19505                    || s.eq_ignore_ascii_case("json_array_elements_text")
19506                    || s.eq_ignore_ascii_case("jsonb_object_keys")
19507                    || s.eq_ignore_ascii_case("json_object_keys")
19508                    || s.eq_ignore_ascii_case("jsonb_path_query")
19509                    || s.eq_ignore_ascii_case("json_path_query")
19510                    || s.eq_ignore_ascii_case("generate_subscripts")
19511                    || s.eq_ignore_ascii_case("string_to_table")
19512                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
19513            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19514        {
19515            let fn_name = match self.peek() {
19516                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19517                _ => unreachable!(),
19518            };
19519            self.advance(); // fn name
19520            self.advance(); // (
19521            let mut fn_args: Vec<Expr> = Vec::new();
19522            loop {
19523                fn_args.push(self.parse_expr(0)?);
19524                if matches!(self.peek(), Token::Comma) {
19525                    self.advance();
19526                    continue;
19527                }
19528                break;
19529            }
19530            if !matches!(self.peek(), Token::RParen) {
19531                return Err(self.err(alloc::format!(
19532                    "expected ')' after {fn_name}() arguments, got {:?}",
19533                    self.peek()
19534                )));
19535            }
19536            self.advance();
19537            let with_ordinality = self.absorb_with_ordinality();
19538            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19539            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19540            // PG's natural column name: the array-elements SRFs
19541            // declare an OUT parameter `value`; jsonb_object_keys
19542            // and generate_subscripts have none, so the column is
19543            // named after the function. A bare table alias on a
19544            // single-column SRF renames the column too (PG: `FROM
19545            // generate_subscripts(a, 1) AS s` projects column s) —
19546            // except for the OUT-parameter SRFs, whose column stays
19547            // `value` under a bare alias.
19548            let natural_col = if fn_name.ends_with("_array_elements")
19549                || fn_name.ends_with("_array_elements_text")
19550            {
19551                "value".to_string()
19552            } else {
19553                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19554            };
19555            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19556            // Keep any further entries — the second names the
19557            // ordinality column under WITH ORDINALITY.
19558            srf_cols.extend(column_aliases.into_iter().skip(1));
19559            // The *_to_table SRFs are row-streams over the existing
19560            // *_to_array scalars — map the call target; the display
19561            // name (alias / column defaults) keeps the SRF spelling.
19562            let call_name = match fn_name.as_str() {
19563                "string_to_table" => "string_to_array".to_string(),
19564                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19565                _ => fn_name,
19566            };
19567            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19568            // preceding FROM item (bare or qualified column) is correlated;
19569            // route it through the per-outer-row lateral channel.
19570            let expr = crate::ast::Expr::FunctionCall {
19571                name: call_name,
19572                args: fn_args,
19573            };
19574            let correlated = Self::expr_has_any_column(&expr);
19575            let tref = TableRef {
19576                name,
19577                alias: alias_ident,
19578                only: false,
19579                as_of_segment: None,
19580                unnest_expr: Some(Box::new(expr)),
19581                unnest_column_aliases: srf_cols,
19582                with_ordinality,
19583                generate_series_args: None,
19584                lateral_subquery: None,
19585                jsonb_each_text_arg: None,
19586                table_fn_call: None,
19587                rows_from: None,
19588                json_table: None,
19589                // Each of these returns a BASE type (jsonb / text / int), so the item's
19590                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19591                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19592                scalar_fn_item: !with_ordinality,
19593            };
19594            return Ok(if correlated {
19595                Self::wrap_correlated_srf(tref)
19596            } else {
19597                tref
19598            });
19599        }
19600        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19601        // explicit parallel-zip syntax. Each entry lowers to its
19602        // array-returning scalar form (unnest(x) → x itself; the
19603        // FROM-SRF rewrite family → their scalar array calls) and
19604        // the list rides the multi-arg unnest zip channel:
19605        // NULL-padded to the longest, WITH ORDINALITY appends the
19606        // counter. generate_series has no scalar array form and
19607        // errors honestly.
19608        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19609            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19610            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19611        {
19612            self.advance(); // ROWS
19613            self.advance(); // FROM
19614            self.advance(); // (
19615            let mut entries: Vec<Expr> = Vec::new();
19616            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19617            // Used only when some entry has no array form.
19618            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19619            loop {
19620                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19621                if !matches!(self.peek(), Token::LParen) {
19622                    return Err(self.err(alloc::format!(
19623                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19624                        self.peek()
19625                    )));
19626                }
19627                self.advance();
19628                let mut fn_args: Vec<Expr> = Vec::new();
19629                if !matches!(self.peek(), Token::RParen) {
19630                    loop {
19631                        fn_args.push(self.parse_expr(0)?);
19632                        if matches!(self.peek(), Token::Comma) {
19633                            self.advance();
19634                            continue;
19635                        }
19636                        break;
19637                    }
19638                }
19639                if !matches!(self.peek(), Token::RParen) {
19640                    return Err(self.err(alloc::format!(
19641                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19642                        self.peek()
19643                    )));
19644                }
19645                self.advance();
19646                let entry = match fn_name.as_str() {
19647                    "unnest" => {
19648                        if fn_args.len() != 1 {
19649                            return Err(
19650                                self.err("unnest inside ROWS FROM takes exactly one array".into())
19651                            );
19652                        }
19653                        fn_args.pop().expect("len checked")
19654                    }
19655                    "jsonb_array_elements"
19656                    | "json_array_elements"
19657                    | "jsonb_array_elements_text"
19658                    | "json_array_elements_text"
19659                    | "jsonb_object_keys"
19660                    | "json_object_keys"
19661                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19662                        name: fn_name,
19663                        args: fn_args,
19664                    },
19665                    "string_to_table" => crate::ast::Expr::FunctionCall {
19666                        name: "string_to_array".to_string(),
19667                        args: fn_args,
19668                    },
19669                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19670                        name: "regexp_split_to_array".to_string(),
19671                        args: fn_args,
19672                    },
19673                    // v7.39 (read01 round 74) — an SRF with no array form
19674                    // (`generate_series`, a user `RETURNS SETOF` function) has no
19675                    // scalar expression to zip, so the WHOLE list switches to the
19676                    // rows_from channel, which runs each function and zips the
19677                    // rows themselves. The all-array case keeps the old lowering:
19678                    // it is well-trodden and this must not disturb it.
19679                    _ => {
19680                        generic.push((fn_name, fn_args));
19681                        if matches!(self.peek(), Token::Comma) {
19682                            self.advance();
19683                            continue;
19684                        }
19685                        break;
19686                    }
19687                };
19688                generic.push((
19689                    // The array-able entries carry their lowered expr along, so a
19690                    // MIXED list still works: the engine sees the scalar array
19691                    // form and unnests it.
19692                    "__array".to_string(),
19693                    alloc::vec![entry.clone()],
19694                ));
19695                entries.push(entry);
19696                if matches!(self.peek(), Token::Comma) {
19697                    self.advance();
19698                    continue;
19699                }
19700                break;
19701            }
19702            if !matches!(self.peek(), Token::RParen) {
19703                return Err(self.err(alloc::format!(
19704                    "expected ')' to close ROWS FROM, got {:?}",
19705                    self.peek()
19706                )));
19707            }
19708            self.advance();
19709            let with_ordinality = self.absorb_with_ordinality();
19710            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19711            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19712            // v7.39 (read01 round 74) — some entry had no array form, so the whole
19713            // list rides the generic channel.
19714            if generic.iter().any(|(n, _)| n != "__array") {
19715                let correlated = generic
19716                    .iter()
19717                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19718                let tref = TableRef {
19719                    name,
19720                    alias: alias_ident,
19721                    only: false,
19722                    as_of_segment: None,
19723                    unnest_expr: None,
19724                    unnest_column_aliases,
19725                    with_ordinality,
19726                    generate_series_args: None,
19727                    lateral_subquery: None,
19728                    jsonb_each_text_arg: None,
19729                    table_fn_call: None,
19730                    rows_from: Some(generic),
19731                    json_table: None,
19732                    scalar_fn_item: false,
19733                };
19734                return Ok(if correlated {
19735                    Self::wrap_correlated_srf(tref)
19736                } else {
19737                    tref
19738                });
19739            }
19740            let correlated = entries.iter().any(Self::expr_has_any_column);
19741            let expr = if entries.len() == 1 {
19742                entries.pop().expect("len checked")
19743            } else {
19744                crate::ast::Expr::FunctionCall {
19745                    name: "__unnest_zip".to_string(),
19746                    args: entries,
19747                }
19748            };
19749            let tref = TableRef {
19750                name,
19751                alias: alias_ident,
19752                only: false,
19753                as_of_segment: None,
19754                unnest_expr: Some(Box::new(expr)),
19755                unnest_column_aliases,
19756                with_ordinality,
19757                generate_series_args: None,
19758                lateral_subquery: None,
19759                jsonb_each_text_arg: None,
19760                table_fn_call: None,
19761                rows_from: None,
19762                json_table: None,
19763                scalar_fn_item: false,
19764            };
19765            return Ok(if correlated {
19766                Self::wrap_correlated_srf(tref)
19767            } else {
19768                tref
19769            });
19770        }
19771        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19772        // source. Detect at the head before the bare-ident fallback;
19773        // unnest is not a reserved token.
19774        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19775            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19776        {
19777            self.advance(); // unnest
19778            self.advance(); // (
19779            let mut srf_args = alloc::vec![self.parse_expr(0)?];
19780            while matches!(self.peek(), Token::Comma) {
19781                self.advance();
19782                srf_args.push(self.parse_expr(0)?);
19783            }
19784            if !matches!(self.peek(), Token::RParen) {
19785                return Err(self.err(alloc::format!(
19786                    "expected ')' after unnest() argument, got {:?}",
19787                    self.peek()
19788                )));
19789            }
19790            self.advance();
19791            // Multi-arg unnest(a, b, …) zips the arrays in
19792            // parallel, NULL-padding to the longest (PG's ROWS
19793            // FROM shorthand). Lower onto the unnest channel as an
19794            // internal marker call the executors unpack.
19795            let expr = if srf_args.len() == 1 {
19796                srf_args.pop().expect("len checked")
19797            } else {
19798                crate::ast::Expr::FunctionCall {
19799                    name: "__unnest_zip".to_string(),
19800                    args: srf_args,
19801                }
19802            };
19803            let with_ordinality = self.absorb_with_ordinality();
19804            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19805            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19806            let correlated = Self::expr_has_any_column(&expr);
19807            let tref = TableRef {
19808                name,
19809                alias: alias_ident,
19810                only: false,
19811                as_of_segment: None,
19812                unnest_expr: Some(Box::new(expr)),
19813                unnest_column_aliases,
19814                with_ordinality,
19815                generate_series_args: None,
19816                lateral_subquery: None,
19817                jsonb_each_text_arg: None,
19818                table_fn_call: None,
19819                rows_from: None,
19820                json_table: None,
19821                scalar_fn_item: false,
19822            };
19823            return Ok(if correlated {
19824                Self::wrap_correlated_srf(tref)
19825            } else {
19826                tref
19827            });
19828        }
19829        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19830        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19831        // generic table-fn arg parser can't read), so it is intercepted
19832        // here BEFORE the generic dispatch. The doc expr may reference
19833        // outer columns (implicit LATERAL) — same correlated-wrap rule.
19834        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19835                if s.eq_ignore_ascii_case("json_table"))
19836            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19837        {
19838            let tref = self.parse_json_table_ref()?;
19839            let correlated = tref
19840                .json_table
19841                .as_deref()
19842                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19843            return Ok(if correlated {
19844                Self::wrap_correlated_srf(tref)
19845            } else {
19846                tref
19847            });
19848        }
19849        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19850        // functions dispatched by name (`pg_partition_tree('t')`,
19851        // `pg_partition_ancestors('t')`). Same head-detection shape as
19852        // unnest; the engine executor owns the row shape per function.
19853        // v7.39 (read01 round 65) — and a USER function in FROM position
19854        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19855        // (generate_series / unnest / the json_each family) keep it — their arms
19856        // sit further down, so they are excluded here by name rather than by
19857        // ordering. Anything else that is an ident followed by `(` is a table
19858        // function; the engine executor decides whether it is a builtin, a
19859        // set-returning user function, or an error.
19860        // 7.38.1 S5.1 — pg_dump spells its table functions
19861        // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
19862        // strip the pg_catalog prefix here so the same head-detection
19863        // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
19864        // meaning.
19865        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
19866            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19867            && matches!(
19868                self.tokens.get(self.pos + 2),
19869                Some(Token::Ident(_) | Token::QuotedIdent(_))
19870            )
19871            && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
19872        {
19873            self.advance(); // pg_catalog
19874            self.advance(); // .
19875        }
19876        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19877                if !s.eq_ignore_ascii_case("generate_series")
19878                    && !s.eq_ignore_ascii_case("unnest")
19879                    && !is_json_each_name(s))
19880            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19881        {
19882            // Body out-of-line — this parse sits on the FROM/subquery
19883            // recursion chain (debug frame-cliff discipline).
19884            // v7.39 (read01 round 69) — a call whose arguments reference an outer
19885            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19886            // outer row, so it rides the lateral channel. Same rule the unnest
19887            // arm uses.
19888            let tref = self.parse_table_fn_ref()?;
19889            let correlated = tref
19890                .table_fn_call
19891                .as_deref()
19892                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19893            return Ok(if correlated {
19894                Self::wrap_correlated_srf(tref)
19895            } else {
19896                tref
19897            });
19898        }
19899        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19900        // [, step])` set-returning source. Same shape as unnest:
19901        // detect at the head, parse the comma-separated arg list,
19902        // dispatch downstream through the engine's set-returning
19903        // path. Supports integer triplets (mailrs's `WITH row_no AS
19904        // (SELECT * FROM generate_series(1, N))` pattern) and
19905        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19906        // date-range iteration pattern, which pre-3.10 had no
19907        // direct equivalent in SPG).
19908        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19909            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19910        {
19911            self.advance(); // generate_series
19912            self.advance(); // (
19913            let mut args: Vec<Expr> = Vec::new();
19914            loop {
19915                args.push(self.parse_expr(0)?);
19916                if matches!(self.peek(), Token::Comma) {
19917                    self.advance();
19918                    continue;
19919                }
19920                break;
19921            }
19922            if !matches!(self.peek(), Token::RParen) {
19923                return Err(self.err(alloc::format!(
19924                    "expected ')' after generate_series() arguments, got {:?}",
19925                    self.peek()
19926                )));
19927            }
19928            self.advance();
19929            if args.len() < 2 || args.len() > 3 {
19930                return Err(self.err(alloc::format!(
19931                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19932                    args.len()
19933                )));
19934            }
19935            let with_ordinality = self.absorb_with_ordinality();
19936            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19937            let name = alias_ident
19938                .clone()
19939                .unwrap_or_else(|| "generate_series".to_string());
19940            let correlated = args.iter().any(Self::expr_has_any_column);
19941            let tref = TableRef {
19942                name,
19943                alias: alias_ident,
19944                only: false,
19945                as_of_segment: None,
19946                unnest_expr: None,
19947                unnest_column_aliases: column_aliases,
19948                with_ordinality,
19949                generate_series_args: Some(args),
19950                lateral_subquery: None,
19951                jsonb_each_text_arg: None,
19952                table_fn_call: None,
19953                rows_from: None,
19954                json_table: None,
19955                scalar_fn_item: false,
19956            };
19957            return Ok(if correlated {
19958                Self::wrap_correlated_srf(tref)
19959            } else {
19960                tref
19961            });
19962        }
19963        // v7.16.2 — preserve information_schema / pg_catalog
19964        // qualifiers (mailrs round-10 A.3). The generic
19965        // `expect_ident_like` strip silently drops the schema;
19966        // we want the engine to recognise these PG meta tables
19967        // and synthesise rows from the live catalog. Produce a
19968        // synthetic name (`__spg_info_columns` etc.) so the
19969        // engine's SELECT-side router can dispatch without
19970        // clashing with any user-defined `columns` table.
19971        let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
19972            (synth, Some(orig))
19973        } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
19974            (synth, Some(orig))
19975        } else {
19976            (self.expect_ident_like()?, None)
19977        };
19978        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19979        // time-travel clause. Parse BEFORE the alias so the
19980        // alias can still ride at the tail (`tbl AS OF SEGMENT
19981        // '5' alias`). `AS` is a reserved keyword token, while
19982        // `OF` and `SEGMENT` are bare idents.
19983        let as_of_segment = if matches!(self.peek(), Token::As)
19984            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19985        {
19986            self.advance(); // AS
19987            self.advance(); // OF
19988            let kw = match self.peek().clone() {
19989                Token::Ident(s) | Token::QuotedIdent(s) => s,
19990                other => {
19991                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19992                }
19993            };
19994            if !kw.eq_ignore_ascii_case("segment") {
19995                return Err(self.err(format!(
19996                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19997                )));
19998            }
19999            self.advance();
20000            // Segment id literal — accept either a string or
20001            // integer for operator ergonomics.
20002            let id = match self.advance() {
20003                Token::String(s) => s
20004                    .parse::<u32>()
20005                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20006                Token::Integer(n) => u32::try_from(n)
20007                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20008                other => {
20009                    return Err(self.err(format!(
20010                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
20011                    )));
20012                }
20013            };
20014            Some(id)
20015        } else {
20016            None
20017        };
20018        // TABLESAMPLE is not a reserved token — keep the bare-ident
20019        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
20020        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
20021        {
20022            None
20023        } else {
20024            self.parse_optional_alias()?
20025        };
20026        // r1052 — a catalog name rewritten to its synthetic form keeps
20027        // the WRITTEN name as the relation's alias, so `pg_cast.oid`
20028        // still binds after `pg_cast` became `__spg_pg_cast`. PG
20029        // semantics: the visible name of `pg_catalog.pg_cast` IS
20030        // `pg_cast`. Without this, every table-name-qualified column
20031        // on a synthesised catalog answered "missing FROM-clause
20032        // entry" — which is the wall pg_dump hit on its first
20033        // pg_proc/pg_cast query.
20034        let alias = match (&alias, &meta_original) {
20035            (None, Some(orig)) if *orig != name => Some(orig.clone()),
20036            _ => alias,
20037        };
20038        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
20039        // (PG grammar). BERNOULLI lowers to a per-row
20040        // `random() < p/100` conjunct on the enclosing SELECT's
20041        // WHERE — exact row-level Bernoulli semantics. SYSTEM
20042        // shares the lowering: SPG has no page structure to
20043        // sample, and the row-level form returns the same expected
20044        // fraction. REPEATABLE(seed) promises a deterministic
20045        // sample SPG cannot honour yet — honest error rather than
20046        // a silently ignored seed.
20047        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
20048            self.advance();
20049            let method = self.expect_ident_like()?;
20050            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
20051                return Err(self.err(alloc::format!(
20052                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
20053                )));
20054            }
20055            if !matches!(self.peek(), Token::LParen) {
20056                return Err(self.err(alloc::format!(
20057                    "expected '(' after TABLESAMPLE {}, got {:?}",
20058                    method.to_ascii_uppercase(),
20059                    self.peek()
20060                )));
20061            }
20062            self.advance();
20063            let percent = self.parse_expr(0)?;
20064            if !matches!(self.peek(), Token::RParen) {
20065                return Err(self.err(alloc::format!(
20066                    "expected ')' after TABLESAMPLE percentage, got {:?}",
20067                    self.peek()
20068                )));
20069            }
20070            self.advance();
20071            // REPEATABLE(seed) → a deterministic per-row draw seeded by
20072            // `seed`, so the sample is stable across repeats and rescans.
20073            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
20074            let mut sample_seed: Option<Expr> = None;
20075            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
20076                self.advance();
20077                if !matches!(self.peek(), Token::LParen) {
20078                    return Err(self.err(alloc::format!(
20079                        "expected '(' after REPEATABLE, got {:?}",
20080                        self.peek()
20081                    )));
20082                }
20083                self.advance();
20084                let seed = self.parse_expr(0)?;
20085                if !matches!(self.peek(), Token::RParen) {
20086                    return Err(self.err(alloc::format!(
20087                        "expected ')' after REPEATABLE seed, got {:?}",
20088                        self.peek()
20089                    )));
20090                }
20091                self.advance();
20092                sample_seed = Some(seed);
20093            }
20094            let draw = match sample_seed {
20095                Some(seed) => Expr::FunctionCall {
20096                    name: "__tsm_fract".to_string(),
20097                    args: alloc::vec![seed],
20098                },
20099                None => Expr::FunctionCall {
20100                    name: "random".to_string(),
20101                    args: Vec::new(),
20102                },
20103            };
20104            self.pending_sample_preds.push(Expr::Binary {
20105                lhs: Box::new(draw),
20106                op: crate::ast::BinOp::Lt,
20107                rhs: Box::new(Expr::Binary {
20108                    lhs: Box::new(percent),
20109                    op: crate::ast::BinOp::Div,
20110                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
20111                }),
20112            });
20113        }
20114        Ok(TableRef {
20115            name,
20116            alias,
20117            only,
20118            as_of_segment,
20119            unnest_expr: None,
20120            unnest_column_aliases: Vec::new(),
20121            with_ordinality: false,
20122            generate_series_args: None,
20123            lateral_subquery: None,
20124            jsonb_each_text_arg: None,
20125            table_fn_call: None,
20126            rows_from: None,
20127            json_table: None,
20128            scalar_fn_item: false,
20129        })
20130    }
20131
20132    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
20133    /// but also accepts `AS alias(col [, col, …])` — the
20134    /// PG-standard table-function column-list form. The column
20135    /// list is only honoured when paired with `UNNEST(...)` in
20136    /// the parent; other call sites currently discard it.
20137    /// True when the expression tree contains a qualified column
20138    /// reference (`t.col`) — the syntactic marker that an SRF
20139    /// argument correlates with a preceding FROM item.
20140    fn expr_has_qualified_column(e: &Expr) -> bool {
20141        match e {
20142            Expr::Column(c) => c.qualifier.is_some(),
20143            Expr::Binary { lhs, rhs, .. } => {
20144                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
20145            }
20146            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
20147            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
20148            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
20149            Expr::Case {
20150                operand,
20151                branches,
20152                else_branch,
20153            } => {
20154                operand
20155                    .as_deref()
20156                    .is_some_and(Self::expr_has_qualified_column)
20157                    || branches.iter().any(|(w, t)| {
20158                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
20159                    })
20160                    || else_branch
20161                        .as_deref()
20162                        .is_some_and(Self::expr_has_qualified_column)
20163            }
20164            _ => false,
20165        }
20166    }
20167
20168    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
20169    /// counts a bare (unqualified) column. A set-returning function has no
20170    /// input columns of its own, so ANY column in its arguments is an outer
20171    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
20172    fn expr_has_any_column(e: &Expr) -> bool {
20173        match e {
20174            Expr::Column(_) => true,
20175            Expr::Binary { lhs, rhs, .. } => {
20176                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
20177            }
20178            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
20179            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
20180            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
20181            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
20182            // constructor or subscript fell to the `_ => false` arm, so
20183            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
20184            // channel and the eager peer eval answered `column "x" does
20185            // not exist` (the substitution walker already recurses both
20186            // shapes; only this detector was blind to them).
20187            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
20188            Expr::ArraySubscript { target, index } => {
20189                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
20190            }
20191            Expr::Case {
20192                operand,
20193                branches,
20194                else_branch,
20195            } => {
20196                operand.as_deref().is_some_and(Self::expr_has_any_column)
20197                    || branches
20198                        .iter()
20199                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
20200                    || else_branch
20201                        .as_deref()
20202                        .is_some_and(Self::expr_has_any_column)
20203            }
20204            _ => false,
20205        }
20206    }
20207
20208    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
20209    /// `generate_series(1, t.n)`) into the lateral_subquery
20210    /// channel: `SELECT * FROM <srf>` executes per outer row with
20211    /// outer references substituted (v7.37.43-T4.5 machinery).
20212    /// Uncorrelated SRFs stay on their plain channels.
20213    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
20214        let name = srf.name.clone();
20215        let alias = srf.alias.clone();
20216        let inner = crate::ast::SelectStatement {
20217            locking: None,
20218            ctes: Vec::new(),
20219            distinct: false,
20220            distinct_on: Vec::new(),
20221            items: alloc::vec![crate::ast::SelectItem::Wildcard],
20222            from: Some(crate::ast::FromClause {
20223                primary: srf,
20224                joins: Vec::new(),
20225            }),
20226            where_: None,
20227            group_by: None,
20228            group_by_all: false,
20229            having: None,
20230            unions: Vec::new(),
20231            order_by: Vec::new(),
20232            limit: None,
20233            offset: None,
20234            limit_with_ties: false,
20235            window_check_exprs: Vec::new(),
20236        };
20237        TableRef {
20238            name,
20239            alias,
20240            only: false,
20241            as_of_segment: None,
20242            unnest_expr: None,
20243            unnest_column_aliases: Vec::new(),
20244            with_ordinality: false,
20245            generate_series_args: None,
20246            lateral_subquery: Some(Box::new(inner)),
20247            jsonb_each_text_arg: None,
20248            table_fn_call: None,
20249            rows_from: None,
20250            json_table: None,
20251            scalar_fn_item: false,
20252        }
20253    }
20254
20255    /// True when the expression tree contains an unresolved
20256    /// `OVER w` marker (see parse_over_clause).
20257    fn expr_has_named_window(e: &Expr) -> bool {
20258        match e {
20259            Expr::WindowFunction { partition_by, .. } => matches!(
20260                partition_by.as_slice(),
20261                [Expr::Column(c)] if matches!(
20262                    c.qualifier.as_deref(),
20263                    Some("__named_window__") | Some("__named_window_ref__")
20264                )
20265            ),
20266            Expr::Binary { lhs, rhs, .. } => {
20267                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
20268            }
20269            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
20270            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
20271            Expr::Case {
20272                operand,
20273                branches,
20274                else_branch,
20275            } => {
20276                operand.as_deref().is_some_and(Self::expr_has_named_window)
20277                    || branches.iter().any(|(w, t)| {
20278                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
20279                    })
20280                    || else_branch
20281                        .as_deref()
20282                        .is_some_and(Self::expr_has_named_window)
20283            }
20284            _ => false,
20285        }
20286    }
20287
20288    /// v7.39 (round 705) — the NAMES the expression references through the
20289    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
20290    /// definitions nothing referenced. Traversal mirrors
20291    /// `expr_has_named_window` above.
20292    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
20293        match e {
20294            Expr::WindowFunction { partition_by, .. } => {
20295                if let [Expr::Column(c)] = partition_by.as_slice()
20296                    && matches!(
20297                        c.qualifier.as_deref(),
20298                        Some("__named_window__") | Some("__named_window_ref__")
20299                    )
20300                {
20301                    into.push(c.name.clone());
20302                }
20303            }
20304            Expr::Binary { lhs, rhs, .. } => {
20305                Self::collect_named_window_refs(lhs, into);
20306                Self::collect_named_window_refs(rhs, into);
20307            }
20308            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20309                Self::collect_named_window_refs(expr, into);
20310            }
20311            Expr::FunctionCall { args, .. } => {
20312                for a in args {
20313                    Self::collect_named_window_refs(a, into);
20314                }
20315            }
20316            Expr::Case {
20317                operand,
20318                branches,
20319                else_branch,
20320            } => {
20321                if let Some(o) = operand.as_deref() {
20322                    Self::collect_named_window_refs(o, into);
20323                }
20324                for (w, t) in branches {
20325                    Self::collect_named_window_refs(w, into);
20326                    Self::collect_named_window_refs(t, into);
20327                }
20328                if let Some(eb) = else_branch.as_deref() {
20329                    Self::collect_named_window_refs(eb, into);
20330                }
20331            }
20332            _ => {}
20333        }
20334    }
20335
20336    /// Inline named-window definitions into the `OVER w` markers.
20337    /// An unknown name errors (PG: window "w" does not exist).
20338    #[allow(clippy::type_complexity)]
20339    fn substitute_named_windows(
20340        e: &mut Expr,
20341        defs: &[(
20342            String,
20343            (
20344                Vec<Expr>,
20345                Vec<(Expr, bool, Option<bool>)>,
20346                Option<WindowFrame>,
20347            ),
20348        )],
20349    ) -> Result<(), String> {
20350        match e {
20351            Expr::WindowFunction {
20352                partition_by,
20353                order_by,
20354                frame,
20355                ..
20356            } => {
20357                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20358                // from the bare `OVER w1` (a plain reference).
20359                let named = match partition_by.as_slice() {
20360                    [Expr::Column(c)] => match c.qualifier.as_deref() {
20361                        Some("__named_window__") => Some((c.name.clone(), false)),
20362                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
20363                        _ => None,
20364                    },
20365                    _ => None,
20366                };
20367                if let Some((wname, is_copy)) = named {
20368                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20369                    else {
20370                        return Err(alloc::format!("window {wname:?} does not exist"));
20371                    };
20372                    if !is_copy {
20373                        *partition_by = def.0.clone();
20374                        *order_by = def.1.clone();
20375                        *frame = def.2.clone();
20376                        return Ok(());
20377                    }
20378                    // v7.39 (round 229) — PG's copy rules, probed against
20379                    // 18.4: a copy inherits the partitioning, may supply an
20380                    // ordering only when the base has none, and may not copy
20381                    // a base that already carries a frame (its own frame
20382                    // would be ambiguous with the inherited one).
20383                    if !def.1.is_empty() && !order_by.is_empty() {
20384                        return Err(alloc::format!(
20385                            "cannot override ORDER BY clause of window \"{wname}\""
20386                        ));
20387                    }
20388                    if def.2.is_some() {
20389                        return Err(alloc::format!(
20390                            "cannot copy window \"{wname}\" because it has a frame clause"
20391                        ));
20392                    }
20393                    *partition_by = def.0.clone();
20394                    if order_by.is_empty() {
20395                        *order_by = def.1.clone();
20396                    }
20397                }
20398                Ok(())
20399            }
20400            Expr::Binary { lhs, rhs, .. } => {
20401                Self::substitute_named_windows(lhs, defs)?;
20402                Self::substitute_named_windows(rhs, defs)
20403            }
20404            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20405                Self::substitute_named_windows(expr, defs)
20406            }
20407            Expr::FunctionCall { args, .. } => {
20408                for a in args {
20409                    Self::substitute_named_windows(a, defs)?;
20410                }
20411                Ok(())
20412            }
20413            Expr::Case {
20414                operand,
20415                branches,
20416                else_branch,
20417            } => {
20418                if let Some(op) = operand {
20419                    Self::substitute_named_windows(op, defs)?;
20420                }
20421                for (w, t) in branches {
20422                    Self::substitute_named_windows(w, defs)?;
20423                    Self::substitute_named_windows(t, defs)?;
20424                }
20425                if let Some(el) = else_branch {
20426                    Self::substitute_named_windows(el, defs)?;
20427                }
20428                Ok(())
20429            }
20430            _ => Ok(()),
20431        }
20432    }
20433
20434    /// SQL-standard `TABLE name` shorthand — builds the equivalent
20435    /// `SELECT * FROM name` head. Callers own set-op chain / tail
20436    /// composition.
20437    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20438        debug_assert!(matches!(self.peek(), Token::Table));
20439        self.advance(); // TABLE
20440        let tname = self.expect_ident_like()?;
20441        Ok(SelectStatement {
20442            locking: None,
20443            ctes: Vec::new(),
20444            distinct: false,
20445            distinct_on: Vec::new(),
20446            items: alloc::vec![SelectItem::Wildcard],
20447            from: Some(FromClause {
20448                primary: TableRef {
20449                    name: tname,
20450                    alias: None,
20451                    only: false,
20452                    as_of_segment: None,
20453                    unnest_expr: None,
20454                    unnest_column_aliases: Vec::new(),
20455                    with_ordinality: false,
20456                    generate_series_args: None,
20457                    lateral_subquery: None,
20458                    jsonb_each_text_arg: None,
20459                    table_fn_call: None,
20460                    rows_from: None,
20461                    json_table: None,
20462                    scalar_fn_item: false,
20463                },
20464                joins: Vec::new(),
20465            }),
20466            where_: None,
20467            group_by: None,
20468            group_by_all: false,
20469            having: None,
20470            unions: Vec::new(),
20471            order_by: Vec::new(),
20472            limit: None,
20473            offset: None,
20474            limit_with_ties: false,
20475            window_check_exprs: Vec::new(),
20476        })
20477    }
20478
20479    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20480    /// variants) → a derived table that reads each declared column out of
20481    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20482    /// `jsonb_array_elements(J)` (one row per element, column `value`);
20483    /// the scalar *record form projects a single row straight off `J`.
20484    /// Rides the existing lateral-subquery channel, so no new executor or
20485    /// AST is needed.
20486    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20487        use crate::ast::{
20488            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20489        };
20490        let fn_name = match self.peek() {
20491            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20492            _ => unreachable!("caller guarded is_json_to_record_name"),
20493        };
20494        self.advance(); // fn name
20495        self.advance(); // (
20496        let mut arg = self.parse_expr(0)?;
20497        // populate_record(base, json): the base only carries the record
20498        // type here — the JSON argument is the second expression.
20499        let mut base: Option<Expr> = None;
20500        if matches!(self.peek(), Token::Comma) {
20501            self.advance();
20502            base = Some(arg);
20503            arg = self.parse_expr(0)?;
20504        }
20505        if !matches!(self.peek(), Token::RParen) {
20506            return Err(self.err(alloc::format!(
20507                "expected ')' after {fn_name}() argument, got {:?}",
20508                self.peek()
20509            )));
20510        }
20511        self.advance(); // )
20512        let is_set = fn_name.ends_with("recordset");
20513        // `[AS] alias ( col type [, …] )` column-definition list.
20514        if matches!(self.peek(), Token::As) {
20515            self.advance();
20516        }
20517        let alias_opt = match self.peek() {
20518            Token::Ident(s) | Token::QuotedIdent(s) => {
20519                let a = s.clone();
20520                self.advance();
20521                Some(a)
20522            }
20523            _ => None,
20524        };
20525        // v7.39 (read01 round 76) — the populate family's canonical PG
20526        // spelling carries no column list at all: the row shape comes from
20527        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20528        // j)`). The parser has no catalog, so hand the two arguments to the
20529        // engine's table-function channel, which does. Only `*_to_record*`
20530        // (whose base is bare `record`) genuinely requires the list.
20531        if !matches!(self.peek(), Token::LParen) {
20532            if let Some(base_expr) = base {
20533                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20534                return Ok(TableRef {
20535                    name: alias.clone(),
20536                    alias: Some(alias),
20537                    only: false,
20538                    as_of_segment: None,
20539                    unnest_expr: None,
20540                    unnest_column_aliases: Vec::new(),
20541                    with_ordinality: false,
20542                    generate_series_args: None,
20543                    lateral_subquery: None,
20544                    jsonb_each_text_arg: None,
20545                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20546                    rows_from: None,
20547                    json_table: None,
20548                    scalar_fn_item: false,
20549                });
20550            }
20551            return Err(self.err(alloc::format!(
20552                "expected '(' to start the {fn_name} column-definition list, got {:?}",
20553                self.peek()
20554            )));
20555        }
20556        let Some(alias) = alias_opt else {
20557            return Err(self.err(alloc::format!(
20558                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20559            )));
20560        };
20561        self.advance(); // (
20562        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20563        loop {
20564            let col = self.expect_ident_like()?;
20565            let ty = self.parse_cast_target()?;
20566            coldefs.push((col, ty));
20567            if matches!(self.peek(), Token::Comma) {
20568                self.advance();
20569                continue;
20570            }
20571            if matches!(self.peek(), Token::RParen) {
20572                self.advance();
20573                break;
20574            }
20575            return Err(self.err(alloc::format!(
20576                "expected ',' or ')' in {fn_name} column list, got {:?}",
20577                self.peek()
20578            )));
20579        }
20580        if coldefs.is_empty() {
20581            return Err(self.err(alloc::format!(
20582                "{fn_name} column-definition list must declare at least one column"
20583            )));
20584        }
20585        // Per column: (base ->> 'col')::type AS col. The base is the
20586        // per-element `value` column for the *set form, or the argument
20587        // itself for the scalar record form.
20588        let items: Vec<SelectItem> = coldefs
20589            .into_iter()
20590            .map(|(col, ty)| {
20591                let base = if is_set {
20592                    Expr::Column(ColumnName {
20593                        qualifier: None,
20594                        name: "value".to_string(),
20595                    })
20596                } else {
20597                    arg.clone()
20598                };
20599                SelectItem::Expr {
20600                    expr: Expr::Cast {
20601                        expr: Box::new(Expr::Binary {
20602                            lhs: Box::new(base),
20603                            op: BinOp::JsonGetText,
20604                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20605                        }),
20606                        target: ty,
20607                    },
20608                    alias: Some(col),
20609                }
20610            })
20611            .collect();
20612        let from = if is_set {
20613            let elem_fn = if fn_name.starts_with("jsonb") {
20614                "jsonb_array_elements"
20615            } else {
20616                "json_array_elements"
20617            };
20618            Some(FromClause {
20619                primary: TableRef {
20620                    name: "value".to_string(),
20621                    alias: None,
20622                    only: false,
20623                    as_of_segment: None,
20624                    unnest_expr: Some(Box::new(Expr::FunctionCall {
20625                        name: elem_fn.to_string(),
20626                        args: alloc::vec![arg],
20627                    })),
20628                    unnest_column_aliases: alloc::vec!["value".to_string()],
20629                    with_ordinality: false,
20630                    generate_series_args: None,
20631                    lateral_subquery: None,
20632                    jsonb_each_text_arg: None,
20633                    table_fn_call: None,
20634                    rows_from: None,
20635                    json_table: None,
20636                    scalar_fn_item: false,
20637                },
20638                joins: Vec::new(),
20639            })
20640        } else {
20641            None
20642        };
20643        let inner = SelectStatement {
20644            locking: None,
20645            ctes: Vec::new(),
20646            distinct: false,
20647            distinct_on: Vec::new(),
20648            items,
20649            from,
20650            where_: None,
20651            group_by: None,
20652            group_by_all: false,
20653            having: None,
20654            unions: Vec::new(),
20655            order_by: Vec::new(),
20656            limit: None,
20657            offset: None,
20658            limit_with_ties: false,
20659            window_check_exprs: Vec::new(),
20660        };
20661        Ok(TableRef {
20662            name: alias.clone(),
20663            alias: Some(alias),
20664            only: false,
20665            as_of_segment: None,
20666            unnest_expr: None,
20667            unnest_column_aliases: Vec::new(),
20668            with_ordinality: false,
20669            generate_series_args: None,
20670            lateral_subquery: Some(Box::new(inner)),
20671            jsonb_each_text_arg: None,
20672            table_fn_call: None,
20673            rows_from: None,
20674            json_table: None,
20675            scalar_fn_item: false,
20676        })
20677    }
20678
20679    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20680    /// Returns true when the clause was present. `WITH` alone (a
20681    /// CTE can never start here) is not enough — the ORDINALITY
20682    /// ident must follow, so a stray WITH still errors downstream.
20683    fn absorb_with_ordinality(&mut self) -> bool {
20684        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20685            && matches!(self.tokens.get(self.pos + 1),
20686                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20687        {
20688            self.advance();
20689            self.advance();
20690            true
20691        } else {
20692            false
20693        }
20694    }
20695
20696    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20697    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20698    /// Out-of-line: the caller sits on the FROM recursion chain.
20699    #[inline(never)]
20700    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20701        let fn_name = match self.advance() {
20702            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20703            _ => unreachable!("caller peeked an ident"),
20704        };
20705        self.advance(); // (
20706        let mut args: Vec<Expr> = Vec::new();
20707        if !matches!(self.peek(), Token::RParen) {
20708            loop {
20709                args.push(self.parse_expr(0)?);
20710                if matches!(self.peek(), Token::Comma) {
20711                    self.advance();
20712                    continue;
20713                }
20714                break;
20715            }
20716        }
20717        if !matches!(self.peek(), Token::RParen) {
20718            return Err(self.err(alloc::format!(
20719                "expected ')' after {fn_name}() arguments, got {:?}",
20720                self.peek()
20721            )));
20722        }
20723        self.advance();
20724        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20725        // counter column rides after the function's own, and the alias list
20726        // names it.
20727        let with_ordinality = self.absorb_with_ordinality();
20728        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20729        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20730        Ok(TableRef {
20731            name,
20732            alias: alias_ident,
20733            only: false,
20734            as_of_segment: None,
20735            unnest_expr: None,
20736            unnest_column_aliases,
20737            with_ordinality,
20738            generate_series_args: None,
20739            lateral_subquery: None,
20740            jsonb_each_text_arg: None,
20741            table_fn_call: Some(Box::new((fn_name, args))),
20742            rows_from: None,
20743            json_table: None,
20744            scalar_fn_item: false,
20745        })
20746    }
20747
20748    /// v7.39 (round 205, JSON_TABLE) — parse
20749    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20750    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20751    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20752    #[inline(never)]
20753    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20754        self.advance(); // json_table
20755        self.advance(); // (
20756        let doc = Box::new(self.parse_expr(0)?);
20757        self.expect_comma_json_table()?;
20758        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20759        // Optional `PASSING <expr> AS <name> [, …]`.
20760        let mut passing: Vec<(String, Expr)> = Vec::new();
20761        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20762            self.advance();
20763            loop {
20764                let e = self.parse_expr(0)?;
20765                if !matches!(self.peek(), Token::As) {
20766                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20767                }
20768                self.advance();
20769                let vname = match self.advance() {
20770                    Token::Ident(s) | Token::QuotedIdent(s) => s,
20771                    other => {
20772                        return Err(self.err(alloc::format!(
20773                            "expected PASSING variable name, got {other:?}"
20774                        )));
20775                    }
20776                };
20777                passing.push((vname, e));
20778                if matches!(self.peek(), Token::Comma) {
20779                    self.advance();
20780                    continue;
20781                }
20782                break;
20783            }
20784        }
20785        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20786            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20787        }
20788        self.advance();
20789        let columns = self.parse_json_table_columns()?;
20790        if !matches!(self.peek(), Token::RParen) {
20791            return Err(self.err(alloc::format!(
20792                "expected ')' to close JSON_TABLE, got {:?}",
20793                self.peek()
20794            )));
20795        }
20796        self.advance();
20797        let alias_ident = self.parse_optional_alias()?;
20798        let name = alias_ident
20799            .clone()
20800            .unwrap_or_else(|| String::from("json_table"));
20801        Ok(TableRef {
20802            name,
20803            alias: alias_ident,
20804            only: false,
20805            as_of_segment: None,
20806            unnest_expr: None,
20807            unnest_column_aliases: Vec::new(),
20808            with_ordinality: false,
20809            generate_series_args: None,
20810            lateral_subquery: None,
20811            jsonb_each_text_arg: None,
20812            table_fn_call: None,
20813            rows_from: None,
20814            json_table: Some(Box::new(crate::ast::JsonTable {
20815                doc,
20816                row_path,
20817                columns,
20818                passing,
20819            })),
20820            scalar_fn_item: false,
20821        })
20822    }
20823
20824    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20825        if !matches!(self.peek(), Token::Comma) {
20826            return Err(self.err(alloc::format!(
20827                "expected ',' after JSON_TABLE document, got {:?}",
20828                self.peek()
20829            )));
20830        }
20831        self.advance();
20832        Ok(())
20833    }
20834
20835    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20836        match self.advance() {
20837            Token::String(s) => Ok(s),
20838            other => Err(self.err(alloc::format!(
20839                "expected {what} string literal, got {other:?}"
20840            ))),
20841        }
20842    }
20843
20844    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20845    #[inline(never)]
20846    fn parse_json_table_columns(
20847        &mut self,
20848    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20849        if !matches!(self.peek(), Token::LParen) {
20850            return Err(self.err("expected '(' after COLUMNS".into()));
20851        }
20852        self.advance();
20853        let mut cols = Vec::new();
20854        loop {
20855            cols.push(self.parse_json_table_one_column()?);
20856            if matches!(self.peek(), Token::Comma) {
20857                self.advance();
20858                continue;
20859            }
20860            break;
20861        }
20862        if !matches!(self.peek(), Token::RParen) {
20863            return Err(self.err(alloc::format!(
20864                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20865                self.peek()
20866            )));
20867        }
20868        self.advance();
20869        Ok(cols)
20870    }
20871
20872    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20873        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20874        // NESTED [PATH] '<p>' COLUMNS (...)
20875        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20876            self.advance();
20877            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20878                self.advance();
20879            }
20880            let path = self.parse_json_string_literal("NESTED PATH")?;
20881            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20882                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20883            }
20884            self.advance();
20885            let columns = self.parse_json_table_columns()?;
20886            return Ok(JsonTableColumn::Nested { path, columns });
20887        }
20888        // <name> ...
20889        let name = match self.advance() {
20890            Token::Ident(s) | Token::QuotedIdent(s) => s,
20891            other => {
20892                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20893            }
20894        };
20895        // <name> FOR ORDINALITY
20896        if matches!(self.peek(), Token::For) {
20897            self.advance();
20898            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20899                return Err(self.err("expected ORDINALITY after FOR".into()));
20900            }
20901            self.advance();
20902            return Ok(JsonTableColumn::Ordinality { name });
20903        }
20904        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20905        let ty = self.parse_column_type_name()?;
20906        let mut format_json = false;
20907        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20908            self.advance();
20909            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20910                return Err(self.err("expected JSON after FORMAT".into()));
20911            }
20912            self.advance();
20913            format_json = true;
20914        }
20915        let mut exists = false;
20916        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20917            self.advance();
20918            exists = true;
20919        }
20920        let mut path = alloc::format!("$.{name}");
20921        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20922            self.advance();
20923            path = self.parse_json_string_literal("column PATH")?;
20924        }
20925        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20926            // `FORMAT JSON` after PATH (alternate placement).
20927            self.advance();
20928            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20929                self.advance();
20930            }
20931            format_json = true;
20932        }
20933        let mut wrapper = false;
20934        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20935            self.advance();
20936            // optional CONDITIONAL/UNCONDITIONAL
20937            if matches!(self.peek(), Token::Ident(s)
20938                if s.eq_ignore_ascii_case("unconditional")
20939                    || s.eq_ignore_ascii_case("conditional"))
20940            {
20941                self.advance();
20942            }
20943            if !matches!(self.peek(), Token::Ident(s)
20944                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20945            {
20946                return Err(self.err("expected WRAPPER after WITH".into()));
20947            }
20948            self.advance();
20949            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20950            if matches!(self.peek(), Token::Ident(s)
20951                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20952            {
20953                self.advance();
20954            }
20955            wrapper = true;
20956        }
20957        // ON EMPTY / ON ERROR clauses (two, in any order).
20958        let mut on_empty = JsonTableOnBehavior::Null;
20959        let mut on_error = JsonTableOnBehavior::Null;
20960        for _ in 0..2 {
20961            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20962            {
20963                self.advance();
20964                Some(JsonTableOnBehavior::Error)
20965            } else if matches!(self.peek(), Token::Null) {
20966                self.advance();
20967                Some(JsonTableOnBehavior::Null)
20968            } else if matches!(self.peek(), Token::Default) {
20969                self.advance();
20970                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20971            } else {
20972                None
20973            };
20974            let Some(behavior) = behavior else { break };
20975            // `ON {EMPTY|ERROR}`
20976            if !matches!(self.peek(), Token::On) {
20977                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20978            }
20979            self.advance();
20980            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20981                self.advance();
20982                on_empty = behavior;
20983            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20984                self.advance();
20985                on_error = behavior;
20986            } else {
20987                return Err(self.err("expected EMPTY or ERROR after ON".into()));
20988            }
20989        }
20990        Ok(JsonTableColumn::Regular {
20991            name,
20992            ty,
20993            path,
20994            exists,
20995            format_json,
20996            wrapper,
20997            on_empty,
20998            on_error,
20999        })
21000    }
21001
21002    fn parse_optional_alias_with_columns(
21003        &mut self,
21004    ) -> Result<(Option<String>, Vec<String>), ParseError> {
21005        let alias = self.parse_optional_alias()?;
21006        if alias.is_none() {
21007            return Ok((None, Vec::new()));
21008        }
21009        let mut cols: Vec<String> = Vec::new();
21010        if matches!(self.peek(), Token::LParen) {
21011            self.advance();
21012            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
21013                self.advance();
21014                cols.push(s);
21015                if matches!(self.peek(), Token::Comma) {
21016                    self.advance();
21017                    continue;
21018                }
21019                break;
21020            }
21021            if matches!(self.peek(), Token::RParen) {
21022                self.advance();
21023            }
21024        }
21025        Ok((alias, cols))
21026    }
21027
21028    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
21029    /// whose keyword token was already consumed and whose `(` is the
21030    /// current token. Factored out of `parse_atom` (and marked
21031    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
21032    /// recursive `parse_atom` frame — inlining them there enlarges the
21033    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
21034    /// against, risking an overflow before the budget triggers.
21035    #[inline(never)]
21036    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21037        self.advance(); // (
21038        let mut args = Vec::new();
21039        if !matches!(self.peek(), Token::RParen) {
21040            loop {
21041                args.push(self.parse_expr(0)?);
21042                match self.peek() {
21043                    Token::Comma => {
21044                        self.advance();
21045                    }
21046                    Token::RParen => break,
21047                    other => {
21048                        return Err(self.err(alloc::format!(
21049                            "expected ',' or ')' in {name}() args, got {other:?}"
21050                        )));
21051                    }
21052                }
21053            }
21054        }
21055        self.advance(); // )
21056        Ok(Expr::FunctionCall {
21057            name: name.into(),
21058            args,
21059        })
21060    }
21061
21062    /// FROM-clause: a primary table reference plus zero-or-more joined
21063    /// peers expressed via either `, <table>` (cross-product, no ON) or
21064    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
21065    /// v1.10 keeps the join list flat (left-associative nested-loop
21066    /// semantics).
21067    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
21068        let primary = self.parse_table_ref()?;
21069        let primary_qual = primary
21070            .alias
21071            .clone()
21072            .unwrap_or_else(|| primary.name.clone());
21073        let joins = self.parse_from_joins(&primary_qual)?;
21074        Ok(FromClause { primary, joins })
21075    }
21076
21077    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
21078    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
21079    /// SAME grammar after its target table has already been consumed.
21080    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
21081    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
21082    /// be parsed forward, once.)
21083    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
21084    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
21085    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
21086    /// desugaring, which needs a name for the left side of each equality.
21087    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
21088        let mut joins = Vec::new();
21089        loop {
21090            // `, <table>` — cross-product with no ON.
21091            if matches!(self.peek(), Token::Comma) {
21092                self.advance();
21093                let table = self.parse_table_ref()?;
21094                joins.push(FromJoin {
21095                    kind: JoinKind::Cross,
21096                    table,
21097                    on: None,
21098                    using_cols: None,
21099                    natural: false,
21100                });
21101                continue;
21102            }
21103            // v7.37.16 — optional leading `NATURAL` before the join
21104            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
21105            // not a lexer keyword (it arrives as a bare Ident), so match
21106            // it case-insensitively here. When present, no ON/USING
21107            // clause is allowed — the common columns are resolved at
21108            // execution time.
21109            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
21110            if natural {
21111                self.advance();
21112            }
21113            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
21114            // CROSS JOIN, and bare JOIN (defaults to INNER).
21115            let kind =
21116                match self.peek() {
21117                    Token::Inner => {
21118                        self.advance();
21119                        if !matches!(self.peek(), Token::Join) {
21120                            return Err(self
21121                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
21122                        }
21123                        self.advance();
21124                        JoinKind::Inner
21125                    }
21126                    Token::Left => {
21127                        self.advance();
21128                        if matches!(self.peek(), Token::Outer) {
21129                            self.advance();
21130                        }
21131                        if !matches!(self.peek(), Token::Join) {
21132                            return Err(self.err(format!(
21133                                "expected JOIN after LEFT [OUTER], got {:?}",
21134                                self.peek()
21135                            )));
21136                        }
21137                        self.advance();
21138                        JoinKind::Left
21139                    }
21140                    Token::Cross => {
21141                        self.advance();
21142                        if !matches!(self.peek(), Token::Join) {
21143                            return Err(self
21144                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
21145                        }
21146                        self.advance();
21147                        JoinKind::Cross
21148                    }
21149                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
21150                    Token::Right => {
21151                        self.advance();
21152                        if matches!(self.peek(), Token::Outer) {
21153                            self.advance();
21154                        }
21155                        if !matches!(self.peek(), Token::Join) {
21156                            return Err(self.err(format!(
21157                                "expected JOIN after RIGHT [OUTER], got {:?}",
21158                                self.peek()
21159                            )));
21160                        }
21161                        self.advance();
21162                        JoinKind::Right
21163                    }
21164                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
21165                    Token::Full => {
21166                        self.advance();
21167                        if matches!(self.peek(), Token::Outer) {
21168                            self.advance();
21169                        }
21170                        if !matches!(self.peek(), Token::Join) {
21171                            return Err(self.err(format!(
21172                                "expected JOIN after FULL [OUTER], got {:?}",
21173                                self.peek()
21174                            )));
21175                        }
21176                        self.advance();
21177                        JoinKind::FullOuter
21178                    }
21179                    Token::Join => {
21180                        self.advance();
21181                        JoinKind::Inner
21182                    }
21183                    _ => break,
21184                };
21185            let table = self.parse_table_ref()?;
21186            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
21187            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
21188            // where prev_table is the most-recent left-side table
21189            // (the previous join's table if any, else the FROM primary).
21190            // PG semantics around column merging are richer (USING'd
21191            // cols become deduplicated single output columns); for
21192            // sugar purposes the predicate-only form covers the
21193            // baseline corpus shape and chained `… JOIN x USING (k)
21194            // JOIN y USING (k)` calls.
21195            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
21196            // common columns resolve at execution time.
21197            if natural {
21198                joins.push(FromJoin {
21199                    kind,
21200                    table,
21201                    on: None,
21202                    using_cols: None,
21203                    natural: true,
21204                });
21205                continue;
21206            }
21207            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
21208            // v7.37.16 — capture the USING column list (in addition to
21209            // the ON desugar below) so the executor can perform PG's
21210            // column-merge on the output side.
21211            let mut using_cols: Option<Vec<String>> = None;
21212            let on = if matches!(self.peek(), Token::On) {
21213                self.advance();
21214                Some(self.parse_expr(0)?)
21215            } else if using_match {
21216                self.advance();
21217                if !matches!(self.peek(), Token::LParen) {
21218                    return Err(
21219                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
21220                    );
21221                }
21222                self.advance();
21223                let mut cols: Vec<String> = Vec::new();
21224                loop {
21225                    match self.peek().clone() {
21226                        Token::Ident(s) | Token::QuotedIdent(s) => {
21227                            self.advance();
21228                            cols.push(s);
21229                        }
21230                        other => {
21231                            return Err(self.err(format!(
21232                                "expected column name inside USING (…), got {other:?}"
21233                            )));
21234                        }
21235                    }
21236                    match self.peek() {
21237                        Token::Comma => {
21238                            self.advance();
21239                            continue;
21240                        }
21241                        Token::RParen => {
21242                            self.advance();
21243                            break;
21244                        }
21245                        other => {
21246                            return Err(self.err(format!(
21247                                "expected ',' or ')' inside USING (…), got {other:?}"
21248                            )));
21249                        }
21250                    }
21251                }
21252                if cols.is_empty() {
21253                    return Err(self.err("USING (…) requires at least one column".to_string()));
21254                }
21255                using_cols = Some(cols.clone());
21256                // Pick the left-side alias: prev join's table if any,
21257                // else FROM primary. Use alias when present, else
21258                // table name (PG-equivalent qualifier).
21259                let left_qual: String = joins
21260                    .last()
21261                    .map(|j| {
21262                        j.table
21263                            .alias
21264                            .clone()
21265                            .unwrap_or_else(|| j.table.name.clone())
21266                    })
21267                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
21268                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
21269                let mut iter = cols.into_iter().map(|c| Expr::Binary {
21270                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21271                        qualifier: Some(left_qual.clone()),
21272                        name: c.clone(),
21273                    })),
21274                    op: crate::ast::BinOp::Eq,
21275                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21276                        qualifier: Some(right_qual.clone()),
21277                        name: c,
21278                    })),
21279                });
21280                let first = iter.next().expect("at least one col");
21281                Some(iter.fold(first, |acc, pred| Expr::Binary {
21282                    lhs: alloc::boxed::Box::new(acc),
21283                    op: crate::ast::BinOp::And,
21284                    rhs: alloc::boxed::Box::new(pred),
21285                }))
21286            } else if kind == JoinKind::Cross {
21287                None
21288            } else {
21289                return Err(self.err(format!(
21290                    "expected ON or USING after {:?} JOIN, got {:?}",
21291                    kind,
21292                    self.peek()
21293                )));
21294            };
21295            joins.push(FromJoin {
21296                kind,
21297                table,
21298                on,
21299                using_cols,
21300                natural: false,
21301            });
21302        }
21303        Ok(joins)
21304    }
21305
21306    /// Optional alias after an expression or table:
21307    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
21308    /// accepted (PG-style implicit alias). Returns `None` if the next token
21309    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
21310    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
21311        if matches!(self.peek(), Token::As) {
21312            self.advance();
21313            // v7.39 (round 340, V56) — after AS the next token MUST be an
21314            // identifier. This used to return None and "let the caller
21315            // surface the error on the next expectation", but when AS is
21316            // the LAST token there is no next expectation: `SELECT 1 AS`
21317            // parsed clean and silently dropped the alias. PG rejects it.
21318            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
21319                return self.expect_ident_like().map(Some);
21320            }
21321            return Err(self.err(alloc::format!(
21322                "expected an alias after AS, got {:?}",
21323                self.peek()
21324            )));
21325        }
21326        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21327        // grammar reserves a long list of follow-keywords from the
21328        // alias slot. SPG's bareword approximation: skip a small
21329        // set of idents that would otherwise be swallowed as the
21330        // table alias and break trailing clauses like CREATE
21331        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21332        // CONFLICT WHERE shapes.
21333        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21334            if is_alias_stopword(s) {
21335                return Ok(None);
21336            }
21337            return Ok(self.expect_ident_like().ok());
21338        }
21339        Ok(None)
21340    }
21341
21342    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21343    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21344        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21345        // error beats a stack overflow (an overflow aborts the
21346        // embedding host process).
21347        self.enter_nested()?;
21348        let r = self.parse_expr_inner(min_prec);
21349        self.nest_depth -= 1;
21350        r
21351    }
21352
21353    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21354    /// When the upcoming tokens form one, return the underlying
21355    /// operator token and the position just past the closing paren
21356    /// so the binary loop can dispatch on the plain operator.
21357    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21358        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21359            return None;
21360        }
21361        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21362            return None;
21363        }
21364        let mut i = self.pos + 2;
21365        // Optional schema qualifier (pg_catalog.<op> etc.).
21366        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21367            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21368        {
21369            i += 2;
21370        }
21371        let op_tok = self.tokens.get(i)?.clone();
21372        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21373            return None;
21374        }
21375        Some((i + 2, op_tok))
21376    }
21377
21378    /// PG operator symbols that lower onto function calls in
21379    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21380    /// family → regexp_like, comparison rung), `^@` (starts_with,
21381    /// comparison rung), `^` (power, tighter than `*`), `#`
21382    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21383    /// subset of the OR bits so the subtraction never borrows).
21384    fn try_symbol_operator(
21385        &mut self,
21386        lhs: &Expr,
21387        min_prec: u8,
21388    ) -> Result<Option<Expr>, ParseError> {
21389        enum Sym {
21390            Regex { ci: bool, negated: bool },
21391            Like { ci: bool, negated: bool },
21392            StartsWith,
21393            Power,
21394            Xor,
21395            RangeAdjacent,
21396        }
21397        // v7.39 (IS-precedence knife) — the low-precedence postfix
21398        // predicates ride this existing leaf call (zero new frame slots
21399        // on the nesting chain).
21400        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21401            return Ok(Some(e));
21402        }
21403        let (sym, prec): (Sym, u8) = match self.peek() {
21404            Token::Tilde => (
21405                Sym::Regex {
21406                    ci: false,
21407                    negated: false,
21408                },
21409                5,
21410            ),
21411            Token::TildeStar => (
21412                Sym::Regex {
21413                    ci: true,
21414                    negated: false,
21415                },
21416                5,
21417            ),
21418            Token::NotTilde => (
21419                Sym::Regex {
21420                    ci: false,
21421                    negated: true,
21422                },
21423                5,
21424            ),
21425            Token::NotTildeStar => (
21426                Sym::Regex {
21427                    ci: true,
21428                    negated: true,
21429                },
21430                5,
21431            ),
21432            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21433            Token::DoubleTilde => (
21434                Sym::Like {
21435                    ci: false,
21436                    negated: false,
21437                },
21438                5,
21439            ),
21440            Token::DoubleTildeStar => (
21441                Sym::Like {
21442                    ci: true,
21443                    negated: false,
21444                },
21445                5,
21446            ),
21447            Token::NotDoubleTilde => (
21448                Sym::Like {
21449                    ci: false,
21450                    negated: true,
21451                },
21452                5,
21453            ),
21454            Token::NotDoubleTildeStar => (
21455                Sym::Like {
21456                    ci: true,
21457                    negated: true,
21458                },
21459                5,
21460            ),
21461            Token::CaretAt => (Sym::StartsWith, 5),
21462            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21463            // tighter than `* / & |`, which the prec-9 rung preserves —
21464            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21465            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21466            Token::Caret => (Sym::Power, 9),
21467            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21468            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21469            Token::Hash => (Sym::Xor, 6),
21470            Token::Adjacent => (Sym::RangeAdjacent, 5),
21471            _ => return Ok(None),
21472        };
21473        if prec < min_prec {
21474            return Ok(None);
21475        }
21476        self.advance();
21477        let rhs = self.parse_expr(prec + 1)?;
21478        let out = match sym {
21479            Sym::Regex { ci, negated } => {
21480                let mut args = alloc::vec![lhs.clone(), rhs];
21481                if ci {
21482                    args.push(Expr::Literal(Literal::String(String::from("i"))));
21483                }
21484                maybe_not(
21485                    Expr::FunctionCall {
21486                        name: String::from("regexp_like"),
21487                        args,
21488                    },
21489                    negated,
21490                )
21491            }
21492            Sym::Like { ci, negated } => Expr::Like {
21493                expr: alloc::boxed::Box::new(lhs.clone()),
21494                pattern: alloc::boxed::Box::new(rhs),
21495                negated,
21496                case_insensitive: ci,
21497            },
21498            Sym::StartsWith => Expr::FunctionCall {
21499                name: String::from("starts_with"),
21500                args: alloc::vec![lhs.clone(), rhs],
21501            },
21502            Sym::Power => Expr::FunctionCall {
21503                name: String::from("power"),
21504                args: alloc::vec![lhs.clone(), rhs],
21505            },
21506            // `#` bitwise XOR — a real operator now (was desugared to
21507            // `(a|b)-(a&b)`, algebraically identical for integers but
21508            // undefined for bit strings; the direct op handles both).
21509            Sym::Xor => Expr::Binary {
21510                lhs: Box::new(lhs.clone()),
21511                op: BinOp::BitXor,
21512                rhs: Box::new(rhs),
21513            },
21514            // range `-|-` "is adjacent to" — lowered to a catalog function.
21515            Sym::RangeAdjacent => Expr::FunctionCall {
21516                name: String::from("range_adjacent"),
21517                args: alloc::vec![lhs.clone(), rhs],
21518            },
21519        };
21520        Ok(Some(out))
21521    }
21522
21523    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
21524    /// predicates, moved out of the tight postfix-cast loop: PG binds
21525    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
21526    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
21527    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
21528    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21529    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21530    /// when nothing at this position belongs to the family. Out-of-line
21531    /// (`inline(never)`): the caller sits on the per-nesting-level frame
21532    /// chain that MAX_NEST_DEPTH is tuned against.
21533    #[inline(never)]
21534    fn parse_postfix_predicate(
21535        &mut self,
21536        lhs: &Expr,
21537        min_prec: u8,
21538    ) -> Result<Option<Expr>, ParseError> {
21539        // Reached through try_symbol_operator (an existing leaf call of
21540        // the binary loop) so NO new stack slots land on the per-nesting
21541        // frame chain; the lhs clones only when a predicate actually
21542        // consumes it.
21543        match self.peek() {
21544            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21545            // comparison family rung 5 (each +1 from the pre-XOR ladder).
21546            Token::Is if min_prec <= 4 => {}
21547            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21548            Token::Not
21549                if min_prec <= 5
21550                    && matches!(
21551                        self.tokens.get(self.pos + 1),
21552                        Some(Token::Between | Token::In | Token::Like)
21553                    ) => {}
21554            Token::Not | Token::Ident(_)
21555                if min_prec <= 5
21556                    && (matches!(self.peek(), Token::Ident(s)
21557                            if s.eq_ignore_ascii_case("ilike")
21558                                || (self.mysql_dialect
21559                                    && (s.eq_ignore_ascii_case("regexp")
21560                                        || s.eq_ignore_ascii_case("rlike")))
21561                                || (s.eq_ignore_ascii_case("similar")
21562                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21563                        || (matches!(self.peek(), Token::Not)
21564                            && matches!(self.tokens.get(self.pos + 1),
21565                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21566                                    || (self.mysql_dialect
21567                                        && (s.eq_ignore_ascii_case("regexp")
21568                                            || s.eq_ignore_ascii_case("rlike")))
21569                                    || s.eq_ignore_ascii_case("similar")))) => {}
21570            _ => return Ok(None),
21571        }
21572        let mut expr = lhs.clone();
21573        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21574        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21575        if min_prec <= 4 {
21576            if matches!(self.peek(), Token::Is) {
21577                self.advance();
21578                let negated = if matches!(self.peek(), Token::Not) {
21579                    self.advance();
21580                    true
21581                } else {
21582                    false
21583                };
21584                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21585                // mailrs pg_dump.
21586                if matches!(self.peek(), Token::Distinct) {
21587                    self.advance();
21588                    if !matches!(self.peek(), Token::From) {
21589                        return Err(self.err(format!(
21590                            "expected FROM after IS{} DISTINCT, got {:?}",
21591                            if negated { " NOT" } else { "" },
21592                            self.peek()
21593                        )));
21594                    }
21595                    self.advance();
21596                    // Right-hand side: parse at the same precedence
21597                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21598                    // groups as `x IS DISTINCT FROM (a + b)`.
21599                    let rhs = self.parse_expr(5)?;
21600                    let op = if negated {
21601                        BinOp::IsNotDistinctFrom
21602                    } else {
21603                        BinOp::IsDistinctFrom
21604                    };
21605                    expr = Expr::Binary {
21606                        op,
21607                        lhs: Box::new(expr),
21608                        rhs: Box::new(rhs),
21609                    };
21610                    {
21611                        return Ok(Some(expr));
21612                    }
21613                }
21614                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21615                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21616                // Lowers onto pg_is_json(x, kind); NOT wraps the
21617                // call in a logical negation.
21618                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21619                if s.eq_ignore_ascii_case("json"))
21620                {
21621                    self.advance(); // JSON
21622                    let kind = match self.peek() {
21623                        Token::Ident(s) | Token::QuotedIdent(s)
21624                            if matches!(
21625                                s.to_ascii_lowercase().as_str(),
21626                                "value" | "object" | "array" | "scalar"
21627                            ) =>
21628                        {
21629                            let k = s.to_ascii_lowercase();
21630                            self.advance();
21631                            k
21632                        }
21633                        _ => "value".to_string(),
21634                    };
21635                    let call = Expr::FunctionCall {
21636                        name: "pg_is_json".to_string(),
21637                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21638                    };
21639                    expr = if negated {
21640                        Expr::Unary {
21641                            op: UnOp::Not,
21642                            expr: Box::new(call),
21643                        }
21644                    } else {
21645                        call
21646                    };
21647                    {
21648                        return Ok(Some(expr));
21649                    }
21650                }
21651                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21652                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21653                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21654                {
21655                    let form_kw = match self.peek() {
21656                        Token::Ident(s) | Token::QuotedIdent(s)
21657                            if matches!(
21658                                s.to_ascii_uppercase().as_str(),
21659                                "NFC" | "NFD" | "NFKC" | "NFKD"
21660                            ) && matches!(
21661                                self.tokens.get(self.pos + 1),
21662                                Some(Token::Ident(n) | Token::QuotedIdent(n))
21663                                    if n.eq_ignore_ascii_case("normalized")
21664                            ) =>
21665                        {
21666                            Some(s.to_ascii_uppercase())
21667                        }
21668                        _ => None,
21669                    };
21670                    let bare_normalized = form_kw.is_none()
21671                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21672                        if s.eq_ignore_ascii_case("normalized"));
21673                    if form_kw.is_some() || bare_normalized {
21674                        if form_kw.is_some() {
21675                            self.advance(); // form keyword
21676                        }
21677                        self.advance(); // NORMALIZED
21678                        let mut args = alloc::vec![expr];
21679                        if let Some(f) = form_kw {
21680                            args.push(Expr::Literal(Literal::String(f)));
21681                        }
21682                        let call = Expr::FunctionCall {
21683                            name: "is_normalized".to_string(),
21684                            args,
21685                        };
21686                        expr = if negated {
21687                            Expr::Unary {
21688                                op: UnOp::Not,
21689                                expr: Box::new(call),
21690                            }
21691                        } else {
21692                            call
21693                        };
21694                        {
21695                            return Ok(Some(expr));
21696                        }
21697                    }
21698                }
21699                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21700                // three-valued boolean tests. IS TRUE/FALSE never
21701                // return NULL, so they lower to CASE forms whose
21702                // ELSE catches the NULL branch; IS UNKNOWN on a
21703                // boolean is exactly IS NULL.
21704                if matches!(self.peek(), Token::True | Token::False)
21705                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21706                {
21707                    let tok = self.advance();
21708                    let test = match tok {
21709                        Token::True => Some(true),
21710                        Token::False => Some(false),
21711                        _ => None, // UNKNOWN
21712                    };
21713                    // v7.39 (round 328, V45) — kept as what the user
21714                    // wrote. These used to be lowered here into `CASE` /
21715                    // `IS NULL`; the semantics were right but the AST no
21716                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21717                    // was echoed back as
21718                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21719                    expr = Expr::BoolTest {
21720                        expr: Box::new(expr),
21721                        value: test,
21722                        negated,
21723                    };
21724                    {
21725                        return Ok(Some(expr));
21726                    }
21727                }
21728                if !matches!(self.peek(), Token::Null) {
21729                    return Err(self.err(format!(
21730                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21731                    if negated { " NOT" } else { "" },
21732                    self.peek()
21733                )));
21734                }
21735                self.advance();
21736                expr = Expr::IsNull {
21737                    expr: Box::new(expr),
21738                    negated,
21739                };
21740                {
21741                    return Ok(Some(expr));
21742                }
21743            }
21744        }
21745        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21746        if min_prec <= 5 {
21747            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21748            // Look one token ahead so a stray `NOT` not followed by any of
21749            // these flows through to the early return below untouched.
21750            let negated = if matches!(self.peek(), Token::Not) {
21751                let next = self.tokens.get(self.pos + 1);
21752                matches!(next, Some(Token::Between | Token::In | Token::Like))
21753                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21754                    || (self.mysql_dialect
21755                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21756                    || s.eq_ignore_ascii_case("similar"))
21757            } else {
21758                false
21759            };
21760            if negated {
21761                self.advance();
21762            }
21763            if matches!(self.peek(), Token::Between) {
21764                expr = self.parse_between_tail(expr, negated)?;
21765                {
21766                    return Ok(Some(expr));
21767                }
21768            }
21769            if matches!(self.peek(), Token::In) {
21770                if self.suppress_in_tail && !negated {
21771                    // POSITION(sub IN str) — IN belongs to the
21772                    // enclosing function syntax; stop here.
21773                    {
21774                        return Ok(None);
21775                    }
21776                }
21777                expr = self.parse_in_tail(expr, negated)?;
21778                {
21779                    return Ok(Some(expr));
21780                }
21781            }
21782            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21783            // lowers onto the internal __similar_to(expr, pat[, esc]) call
21784            // (the SQL→regex transform runs inside, in the backtracking-
21785            // friendly shape SPG's matcher needs).
21786            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21787                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21788            {
21789                self.advance(); // SIMILAR
21790                self.advance(); // TO
21791                let pattern = self.parse_expr(6)?;
21792                let mut args = alloc::vec![expr, pattern];
21793                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21794                    self.advance();
21795                    args.push(self.parse_expr(6)?);
21796                }
21797                let call = Expr::FunctionCall {
21798                    name: "__similar_to".to_string(),
21799                    args,
21800                };
21801                expr = maybe_not(call, negated);
21802                {
21803                    return Ok(Some(expr));
21804                }
21805            }
21806            if matches!(self.peek(), Token::Like) {
21807                self.advance();
21808                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21809                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21810                    expr = q;
21811                    {
21812                        return Ok(Some(expr));
21813                    }
21814                }
21815                // Pattern at the same precedence as other comparison RHSes —
21816                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21817                let mut pattern = self.parse_expr(6)?;
21818                // `ESCAPE 'c'` — rewrite a literal pattern to the
21819                // default backslash escape at parse time. Custom
21820                // escapes on non-literal patterns would need
21821                // matcher support; error honestly.
21822                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21823                    self.advance();
21824                    let esc = self.parse_expr(6)?;
21825                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21826                }
21827                expr = Expr::Like {
21828                    expr: Box::new(expr),
21829                    pattern: Box::new(pattern),
21830                    negated,
21831                    case_insensitive: false,
21832                };
21833                {
21834                    return Ok(Some(expr));
21835                }
21836            }
21837            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21838            // keyword reaches us as a plain identifier.
21839            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21840                self.advance();
21841                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21842                    expr = q;
21843                    {
21844                        return Ok(Some(expr));
21845                    }
21846                }
21847                let pattern = self.parse_expr(6)?;
21848                expr = Expr::Like {
21849                    expr: Box::new(expr),
21850                    pattern: Box::new(pattern),
21851                    negated,
21852                    case_insensitive: true,
21853                };
21854                {
21855                    return Ok(Some(expr));
21856                }
21857            }
21858            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21859            // operator (RLIKE is the alias). It is a keyword, not `~`, and
21860            // matches case-insensitively under the default collation, so it
21861            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21862            // `~*` operator uses, wrapped in NOT when negated.
21863            if self.mysql_dialect
21864                && matches!(self.peek(), Token::Ident(s)
21865                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21866            {
21867                self.advance();
21868                let pattern = self.parse_expr(6)?;
21869                let call = Expr::FunctionCall {
21870                    name: String::from("regexp_like"),
21871                    args: alloc::vec![
21872                        expr,
21873                        pattern,
21874                        Expr::Literal(Literal::String(String::from("i"))),
21875                    ],
21876                };
21877                return Ok(Some(maybe_not(call, negated)));
21878            }
21879        }
21880        let _ = expr;
21881        Ok(None)
21882    }
21883
21884    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21885        let mut lhs = self.parse_unary()?;
21886        let mut chain_len = 0usize;
21887        loop {
21888            // OPERATOR([schema.]op) reduces to its underlying
21889            // operator token before the normal dispatch.
21890            let explicit = self.peek_explicit_operator();
21891            let dispatch = match &explicit {
21892                Some((_, tok)) => self.binop_here(tok),
21893                None => self.binop_here(self.peek()),
21894            };
21895            let Some((op, prec)) = dispatch else {
21896                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21897                // of the symbol family. `binop_here` answers None for them
21898                // because they lower onto function calls rather than a
21899                // BinOp, and the fallback below reads `self.peek()` — the
21900                // word OPERATOR, not the operator. `pg_dump` writes every
21901                // catalog predicate this way, so its first query failed
21902                // and no dump ran:
21903                //
21904                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21905                //
21906                // Collapsing the wrapper to the operator it names puts the
21907                // token where the fallback already looks.
21908                if let Some((next, op_tok)) = explicit {
21909                    self.tokens.splice(self.pos..next, [op_tok]);
21910                }
21911                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21912                    lhs = e;
21913                    chain_len += 1;
21914                    if chain_len > MAX_BINARY_CHAIN {
21915                        return Err(self.err(alloc::format!(
21916                            "more than {MAX_BINARY_CHAIN} chained binary operators"
21917                        )));
21918                    }
21919                    continue;
21920                }
21921                break;
21922            };
21923            if prec < min_prec {
21924                break;
21925            }
21926            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21927            // iteratively but evaluates and drops recursively;
21928            // depth beyond the budget overflows worker stacks.
21929            chain_len += 1;
21930            if chain_len > MAX_BINARY_CHAIN {
21931                return Err(self.err(alloc::format!(
21932                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21933                )));
21934            }
21935            match explicit {
21936                Some((end_pos, _)) => self.pos = end_pos,
21937                None => {
21938                    self.advance();
21939                }
21940            }
21941            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21942            // ANY is a bare ident; ALL is a reserved Token. Both
21943            // require an immediate `(` to disambiguate from
21944            // identifier columns named `any` / `all`.
21945            let any_kind = match self.peek() {
21946                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21947                    Some(false)
21948                }
21949                Token::Ident(s) | Token::QuotedIdent(s)
21950                    if (s.eq_ignore_ascii_case("any")
21951                        || s.eq_ignore_ascii_case("some")
21952                        || s.eq_ignore_ascii_case("all"))
21953                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21954                {
21955                    Some(!s.eq_ignore_ascii_case("all"))
21956                }
21957                _ => None,
21958            };
21959            if let Some(is_any) = any_kind {
21960                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21961                continue;
21962            }
21963            let rhs = self.parse_expr(prec + 1)?;
21964            lhs = Expr::Binary {
21965                lhs: Box::new(lhs),
21966                op,
21967                rhs: Box::new(rhs),
21968            };
21969        }
21970        Ok(lhs)
21971    }
21972
21973    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21974    /// and the array form.
21975    ///
21976    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21977    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21978    /// this block's `Expr` temporaries and four `format!` sites slots in
21979    /// that frame on every level of `((((1))))`, which never reaches it.
21980    #[inline(never)]
21981    fn parse_any_all_rhs(
21982        &mut self,
21983        lhs: Expr,
21984        op: BinOp,
21985        is_any: bool,
21986    ) -> Result<Expr, ParseError> {
21987        self.advance(); // ident
21988        self.advance(); // (
21989        // `x op ANY (SELECT …)` — the quantified-subquery
21990        // form. `= ANY` is exactly IN; the other operators
21991        // lower onto EXISTS over the subquery as a derived
21992        // table, comparing against its single projection
21993        // aliased __v (x's columns resolve correlated).
21994        // ALL is the negated-EXISTS complement; a NULL
21995        // element makes PG return NULL where this lowering
21996        // returns true — the NOT NULL column case (the
21997        // practical one) is exact.
21998        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21999            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
22000            // legal PG too (round-151 sibling). Out-of-line
22001            // (#[inline(never)] helper) — this sits on
22002            // parse_expr's recursive frame and the two-armed
22003            // SELECT temporary blew the nesting-budget stack.
22004            let mut sub = self.parse_any_all_select_body()?;
22005            if !matches!(self.peek(), Token::RParen) {
22006                return Err(self.err(alloc::format!(
22007                    "expected ')' after ANY/ALL subquery, got {:?}",
22008                    self.peek()
22009                )));
22010            }
22011            self.advance();
22012            if sub.items.len() != 1 {
22013                return Err(self.err(alloc::format!(
22014                    "ANY/ALL subquery must return one column, got {}",
22015                    sub.items.len()
22016                )));
22017            }
22018            if is_any && matches!(op, BinOp::Eq) {
22019                return Ok(Expr::InSubquery {
22020                    expr: Box::new(lhs),
22021                    subquery: Box::new(sub),
22022                    negated: false,
22023                });
22024            }
22025            // The engine's subquery resolvers materialise
22026            // the single-column result into an ARRAY the
22027            // existing AnyAll three-valued eval consumes.
22028            return Ok(Expr::AnyAll {
22029                expr: Box::new(lhs),
22030                op,
22031                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
22032                is_any,
22033            });
22034        }
22035        let arr = self.parse_expr(0)?;
22036        if !matches!(self.peek(), Token::RParen) {
22037            return Err(self.err(alloc::format!(
22038                "expected ')' after ANY/ALL argument, got {:?}",
22039                self.peek()
22040            )));
22041        }
22042        self.advance();
22043        Ok(Expr::AnyAll {
22044            expr: Box::new(lhs),
22045            op,
22046            array: Box::new(arr),
22047            is_any,
22048        })
22049    }
22050
22051    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
22052    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
22053    #[inline(never)]
22054    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
22055        self.advance();
22056        let e = self.parse_expr(9)?;
22057        Ok(build_center_call(e))
22058    }
22059
22060    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
22061    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
22062    /// unary minus.
22063    ///
22064    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
22065    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
22066    /// the Expr-sized local stays out of that frame.
22067    #[inline(never)]
22068    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
22069        self.advance();
22070        let e = self.parse_expr(9)?;
22071        Ok(Expr::FunctionCall {
22072            name: alloc::string::String::from(name),
22073            args: alloc::vec![e],
22074        })
22075    }
22076
22077    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
22078    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
22079    #[inline(never)]
22080    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
22081        self.advance();
22082        let e = self.parse_expr(9)?;
22083        Ok(Expr::FunctionCall {
22084            name: alloc::string::String::from(if vertical {
22085                "isvertical"
22086            } else {
22087                "ishorizontal"
22088            }),
22089            args: alloc::vec![e],
22090        })
22091    }
22092
22093    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
22094    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
22095    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
22096    #[inline(never)]
22097    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
22098        self.advance();
22099        let e = self.parse_expr(9)?;
22100        Ok(Expr::Cast {
22101            expr: Box::new(e),
22102            target: CastTarget::Named("binary".to_string()),
22103        })
22104    }
22105
22106    /// The prefix operators that share one shape: take the token, parse
22107    /// an operand at `prec`, wrap it.
22108    ///
22109    /// `#[inline(never)]`, and one function instead of five arms, for the
22110    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
22111    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
22112    /// debug build gives EVERY arm's locals a slot in the frame, whichever
22113    /// arm runs. `((((1))))` reaches none of these arms and was carrying
22114    /// five `Expr`-sized locals per level for them anyway.
22115    #[inline(never)]
22116    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
22117        self.advance();
22118        let e = self.parse_expr(prec)?;
22119        Ok(Expr::Unary {
22120            op,
22121            expr: Box::new(e),
22122        })
22123    }
22124
22125    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
22126    /// and separate from it because of the literal folding below and the
22127    /// `format!` temporaries that folding needs.
22128    #[inline(never)]
22129    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
22130        self.advance();
22131        // v7.39 (round 549) — fold the sign into an integer literal that
22132        // only fits once it is negative.
22133        //
22134        // `9223372036854775808` is one past i64::MAX, so the lexer hands
22135        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
22136        // folds the sign first, so `-9223372036854775808` is a bigint
22137        // there — and `-9223372036854775808 - 1` raises "bigint out of
22138        // range" where SPG quietly answered -9223372036854775809, a value
22139        // no bigint can hold. The arithmetic itself was already checked;
22140        // only the literal's type was wrong.
22141        if let Token::Numeric(lit) = self.peek()
22142            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
22143        {
22144            self.advance();
22145            return Ok(Expr::Literal(Literal::Integer(folded)));
22146        }
22147        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
22148        // `<->` slotted into 5 and arithmetic shifted up).
22149        let e = self.parse_expr(9)?;
22150        Ok(Expr::Unary {
22151            op: UnOp::Neg,
22152            expr: Box::new(e),
22153        })
22154    }
22155
22156    /// tsquery `!!` prefix negation, lowered to the catalog function.
22157    /// Binds like unary minus. Out-of-line for the frame reason on
22158    /// `parse_unary_op`.
22159    #[inline(never)]
22160    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
22161        self.advance();
22162        let e = self.parse_expr(9)?;
22163        Ok(Expr::FunctionCall {
22164            name: String::from("tsquery_not"),
22165            args: alloc::vec![e],
22166        })
22167    }
22168
22169    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
22170        match self.peek() {
22171            // NOT binds tighter than AND / XOR / OR but looser than
22172            // comparisons — its operand takes everything ≥ the comparison
22173            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
22174            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
22175            // was rung 3, behaviour-identical when 3 was unused; AND now
22176            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
22177            Token::Not => self.parse_unary_op(UnOp::Not, 4),
22178            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
22179            // The body is out-of-line: `parse_unary` is one of the three
22180            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
22181            // inline arm here overflowed the native stack in
22182            // `nesting_budget_errors_cleanly` — the guard test caught it,
22183            // exactly as the eval-side cliff did in rounds 346 and 351.
22184            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
22185                self.parse_binary_prefix()
22186            }
22187            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
22188            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
22189            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
22190            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
22191            Token::Minus => self.parse_prefix_minus(),
22192            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
22193            // worked only because the lexer reads it as one signed literal;
22194            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
22195            // PG18 and MariaDB take all of them. Binds like unary minus.
22196            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
22197            // Bitwise NOT binds like unary minus.
22198            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
22199            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
22200            // "center of" operator; desugars to center(x). The whole arm
22201            // is out-of-line: parse_unary sits on the per-nesting-level
22202            // frame chain that MAX_NEST_DEPTH is tuned against, so no
22203            // Expr-sized local may live in this frame.
22204            Token::TsMatch => self.parse_prefix_center(),
22205            // v7.39 (round 508) — the prefix operators that are named
22206            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
22207            // is length. Out-of-line for the same nesting-frame reason as
22208            // parse_prefix_center — parse_unary sits on the recursive cycle
22209            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
22210            // live in this frame.
22211            Token::At => self.parse_prefix_call("abs"),
22212            Token::Hash => self.parse_prefix_call("npoints"),
22213            Token::AtMinusAt => self.parse_prefix_call("length"),
22214            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
22215            // "is horizontal" (lseg / line); desugars to the existing
22216            // isvertical()/ishorizontal() functions. Out-of-line for the
22217            // same nesting-frame reason as parse_prefix_center.
22218            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
22219            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
22220            Token::DoubleBang => self.parse_prefix_tsquery_not(),
22221            _ => self.parse_atom(),
22222        }
22223    }
22224
22225    /// Parse a parenthesised scalar subquery body after the caller has consumed
22226    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
22227    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
22228    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
22229    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
22230    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
22231    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
22232    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
22233    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
22234    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
22235    /// tips the deep-nesting test into a stack overflow).
22236    #[inline(never)]
22237    fn array_subquery_ahead(&self) -> bool {
22238        if !matches!(self.peek(), Token::LParen) {
22239            return false;
22240        }
22241        matches!(
22242            self.tokens.get(self.pos + 1),
22243            Some(Token::Select | Token::Values)
22244        ) || matches!(
22245            self.tokens.get(self.pos + 1),
22246            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
22247        )
22248    }
22249
22250    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
22251    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
22252    /// locals stay off parse_atom's recursive frame (round 105).
22253    #[inline(never)]
22254    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
22255        self.advance(); // consume `[`
22256        let mut items: Vec<Expr> = Vec::new();
22257        if !matches!(self.peek(), Token::RBracket) {
22258            loop {
22259                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
22260                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
22261                if matches!(self.peek(), Token::LBracket) {
22262                    items.push(self.parse_array_bracket_body()?);
22263                } else {
22264                    items.push(self.parse_expr(0)?);
22265                }
22266                match self.peek() {
22267                    Token::Comma => {
22268                        self.advance();
22269                    }
22270                    Token::RBracket => break,
22271                    other => {
22272                        return Err(self.err(alloc::format!(
22273                            "expected ',' or ']' in ARRAY literal, got {other:?}"
22274                        )));
22275                    }
22276                }
22277            }
22278        }
22279        self.advance(); // consume `]`
22280        Ok(Expr::Array(items))
22281    }
22282
22283    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
22284    /// is already consumed; the current token is `(`. Desugars to a scalar
22285    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
22286    /// the subquery's single-column rows in order — reusing the existing
22287    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
22288    /// keeps the large `Statement` local off parse_atom's recursive frame.
22289    #[inline(never)]
22290    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
22291        self.advance(); // consume `(`
22292        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
22293            if w.eq_ignore_ascii_case("with"));
22294        let sub = if is_with {
22295            self.advance(); // WITH
22296            self.parse_with_cte_then_select()?
22297        } else {
22298            self.parse_select_stmt()?
22299        };
22300        if !matches!(self.peek(), Token::RParen) {
22301            return Err(self.err(alloc::format!(
22302                "expected ')' to close ARRAY(subquery), got {:?}",
22303                self.peek()
22304            )));
22305        }
22306        self.advance(); // consume `)`
22307        // Reuse the parser to build the array_agg wrapper from the subquery's
22308        // canonical text — avoids hand-constructing the derived-table AST.
22309        let wrapper = alloc::format!(
22310            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
22311        );
22312        let stmt = parse_statement(&wrapper)
22313            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
22314        let Statement::Select(sel) = stmt else {
22315            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
22316        };
22317        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
22318    }
22319
22320    #[inline(never)]
22321    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
22322        let inner = if is_with {
22323            self.advance(); // WITH
22324            self.parse_with_cte_then_select()?
22325        } else {
22326            self.parse_select_stmt()?
22327        };
22328        match self.advance() {
22329            Token::RParen => {
22330                let Statement::Select(s) = inner else {
22331                    return Err(ParseError {
22332                        message: "scalar subquery body must be a SELECT".into(),
22333                        token_pos: self.consumed_pos(),
22334                    });
22335                };
22336                Ok(Expr::ScalarSubquery(Box::new(s)))
22337            }
22338            other => Err(ParseError {
22339                message: format!("expected ')' after scalar subquery, got {other:?}"),
22340                token_pos: self.consumed_pos(),
22341            }),
22342        }
22343    }
22344
22345    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22346    /// literals. The lexer splits them into an ident + string; recombine
22347    /// here. Out-of-line and returning `Option` so `parse_atom` — the
22348    /// recursive frame the 768 KiB stack budget is tuned against — pays no
22349    /// frame for the `body` / `bits` strings and their char loops (the
22350    /// round-367 frame cliff, M20).
22351    #[inline(never)]
22352    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22353        let is_hex = match self.peek() {
22354            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22355            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22356            _ => return None,
22357        };
22358        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22359            return None;
22360        }
22361        // v7.39.3 — where the LITERAL starts, because the errors below
22362        // are about the literal and both engines point at it. `err`
22363        // reports the CURRENT token, which by then is the one after the
22364        // string: `SELECT x'123'` pointed at Eof, so the MySQL wire's
22365        // `near '…'` snippet — which runs from the reported position to
22366        // the end — came out empty where MySQL 9.7.2 says `near
22367        // 'x'123''`.
22368        let lit_pos = self.pos;
22369        self.advance();
22370        let Token::String(body) = self.advance() else {
22371            unreachable!("guarded above");
22372        };
22373        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22374        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22375        // (hex pairs, even count required — MariaDB errors on an odd
22376        // count); `b'1010'` packs its bits big-endian, left-padded to a
22377        // byte. Lower both onto the bytea cast.
22378        if self.mysql_dialect {
22379            if is_hex {
22380                if body.len() % 2 == 1 {
22381                    return Some(Err(self.err_at(
22382                        lit_pos,
22383                        alloc::format!("invalid hex string literal X'{body}': odd digit count"),
22384                    )));
22385                }
22386                for c in body.chars() {
22387                    if !c.is_ascii_hexdigit() {
22388                        return Some(Err(self.err_at(
22389                            lit_pos,
22390                            alloc::format!("invalid hexadecimal digit {c:?} in X'…'"),
22391                        )));
22392                    }
22393                }
22394                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22395            }
22396            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22397                return Some(Err(self.err_at(
22398                    lit_pos,
22399                    alloc::format!("invalid binary digit {bad:?} in b'…'"),
22400                )));
22401            }
22402            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22403        }
22404        let bits = if is_hex {
22405            let mut out = String::with_capacity(body.len() * 4);
22406            for c in body.chars() {
22407                let Some(d) = c.to_digit(16) else {
22408                    // v7.39.3 — PostgreSQL 18.6's own sentence, and its
22409                    // own quoting: `"g" is not a valid hexadecimal
22410                    // digit` (measured, with the caret on the literal).
22411                    return Some(Err(self.err_at(
22412                        lit_pos,
22413                        alloc::format!("\"{c}\" is not a valid hexadecimal digit"),
22414                    )));
22415                };
22416                out.push_str(&alloc::format!("{d:04b}"));
22417            }
22418            out
22419        } else {
22420            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22421                return Some(Err(self.err_at(
22422                    lit_pos,
22423                    alloc::format!("\"{bad}\" is not a valid binary digit"),
22424                )));
22425            }
22426            body
22427        };
22428        // Route through the postfix-cast loop so a chained cast like
22429        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22430        // of erroring at the `::`.
22431        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22432        // literal keeps its exact length, while an explicit `::bit` cast is
22433        // bit(1) with pad/truncate semantics (PG).
22434        Some(self.finish_postfix_casts(Expr::Cast {
22435            expr: Box::new(Expr::Literal(Literal::String(bits))),
22436            target: CastTarget::Named("__bit_literal".to_string()),
22437        }))
22438    }
22439
22440    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22441        if let Some(res) = self.try_parse_bit_string_literal() {
22442            return res;
22443        }
22444        let tok_pos = self.pos;
22445        match self.advance() {
22446            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
22447            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
22448            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
22449            // carrying the source mantissa + scale so no precision is lost. A
22450            // literal too wide for i128 falls back to double precision.
22451            // Out-of-line (#[inline(never)]) — this arm sits on the
22452            // parse_expr recursion chain; its expansion locals must not
22453            // widen the recursive frame (debug frame-cliff discipline).
22454            Token::Numeric(s) => match numeric_token_to_literal(s) {
22455                Ok(lit) => Ok(Expr::Literal(lit)),
22456                Err(msg) => Err(self.err(msg)),
22457            },
22458            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
22459            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
22460            // (the lexer only emits this token in the MySQL dialect). Lower
22461            // onto the existing bytea cast; out-of-line to keep this arm off
22462            // the parse recursion frame.
22463            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22464            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22465            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22466            Token::Null => Ok(Expr::Literal(Literal::Null)),
22467            // v6.1.1 — `$N` placeholder. The actual Value lookup
22468            // happens in the engine eval path against the prepared-
22469            // statement bind buffer.
22470            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22471            Token::LParen => {
22472                // v4.10: `(SELECT ...)` in expression position is a
22473                // scalar subquery; otherwise it's a parenthesised
22474                // expression. Peek for SELECT keyword to dispatch.
22475                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22476                // lexes as Ident("with") (not a reserved token). The subquery body
22477                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22478                // so its large `Statement` local stays out of parse_atom's stack
22479                // frame — parse_atom is on the recursive `((…))` cycle and the
22480                // nesting budget is tuned to its frame size).
22481                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22482                    if s.eq_ignore_ascii_case("with"));
22483                if matches!(self.peek(), Token::Select) || is_with {
22484                    self.parse_paren_scalar_subquery(is_with)
22485                } else {
22486                    let e = self.parse_expr(0)?;
22487                    // `(a, b, …)` — a row constructor. Valid only
22488                    // in front of a comparison operator or [NOT]
22489                    // IN; both expand at parse time (lexicographic
22490                    // comparison / OR'd row equalities).
22491                    if matches!(self.peek(), Token::Comma) {
22492                        let mut row = alloc::vec![e];
22493                        while matches!(self.peek(), Token::Comma) {
22494                            self.advance();
22495                            row.push(self.parse_expr(0)?);
22496                        }
22497                        if !matches!(self.peek(), Token::RParen) {
22498                            return Err(self.err(alloc::format!(
22499                                "expected ')' after row constructor, got {:?}",
22500                                self.peek()
22501                            )));
22502                        }
22503                        self.advance();
22504                        // A bare `(a, b, …)` row constructor can carry postfix
22505                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
22506                        // early return here skips parse_atom's tail postfix
22507                        // pass, so fold casts in explicitly. For the
22508                        // comparison / predicate forms nothing postfix follows,
22509                        // so this is a no-op.
22510                        return self
22511                            .parse_row_comparison_tail(row)
22512                            .and_then(|e| self.finish_postfix_casts(e));
22513                    }
22514                    match self.advance() {
22515                        Token::RParen => Ok(e),
22516                        other => Err(ParseError {
22517                            message: format!("expected ')', got {other:?}"),
22518                            token_pos: self.consumed_pos(),
22519                        }),
22520                    }
22521                }
22522            }
22523            Token::LBracket => self.parse_vector_literal_body(),
22524            Token::Extract => self.parse_extract_atom(),
22525            Token::Interval => self.parse_interval_atom(),
22526            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
22527            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
22528            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
22529            // expression position calling the PG `left(string, n)` /
22530            // `right(string, n)` function; rebuild the AST as a regular
22531            // function call so the engine's apply_function dispatch picks
22532            // it up. Delegated to a #[inline(never)] helper so its locals
22533            // don't bloat this recursive `parse_atom` frame (the nesting
22534            // budget in `enter_nested` is tuned to parse_atom's size).
22535            Token::Left if matches!(self.peek(), Token::LParen) => {
22536                self.parse_lr_string_function_call("left")
22537            }
22538            Token::Right if matches!(self.peek(), Token::LParen) => {
22539                self.parse_lr_string_function_call("right")
22540            }
22541            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
22542            // token; we match on the bare ident. NOT is a token
22543            // (consumed in the comparison rung), but `EXISTS (...)`
22544            // at the top of an expression starts here.
22545            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22546                self.parse_exists_atom(false)
22547            }
22548            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22549            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22550            // CASE is a bare ident; we dispatch on lowercase match.
22551            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22552                self.parse_case_atom()
22553            }
22554            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22555            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22556            // '…'`. Lower onto the ::cast node so the existing
22557            // runtime text→date/timestamp paths do the parsing. The
22558            // string must follow immediately, else the ident stays a
22559            // plain column reference.
22560            Token::Ident(s)
22561                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22562                    && matches!(self.peek(), Token::String(_)) =>
22563            {
22564                let target =
22565                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22566                let Token::String(lit) = self.advance() else {
22567                    unreachable!("peek guaranteed a string token");
22568                };
22569                Ok(Expr::Cast {
22570                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22571                    target,
22572                })
22573            }
22574            // v7.39 (round 221) — the SQL-standard long spellings:
22575            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22576            // TIME ZONE '…'`. Consume the modifier and lower to the same
22577            // typed-literal cast (`timetz` / `timestamptz` for WITH).
22578            Token::Ident(s)
22579                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22580                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22581                        || w.eq_ignore_ascii_case("without"))
22582                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22583                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22584                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22585            {
22586                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22587                self.advance(); // WITH / WITHOUT
22588                self.advance(); // TIME
22589                self.advance(); // ZONE
22590                let Token::String(lit) = self.advance() else {
22591                    unreachable!("guard checked a string token");
22592                };
22593                let base = s.to_ascii_lowercase();
22594                let target = match (base.as_str(), with_tz) {
22595                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22596                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22597                    (_, true) => CastTarget::Timestamptz,
22598                    (_, false) => CastTarget::Timestamp,
22599                };
22600                Ok(Expr::Cast {
22601                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22602                    target,
22603                })
22604            }
22605            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22606            // gathers the subquery's single-column rows (in its row order)
22607            // into an array. Desugared to `array_agg` over the subquery as a
22608            // derived table; out-of-line to keep parse_atom's frame small (it
22609            // sits on the recursive nesting-budget cycle).
22610            Token::Ident(s) | Token::QuotedIdent(s)
22611                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22612            {
22613                self.parse_array_subquery()
22614            }
22615            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22616            // is not a reserved token; we match by case-insensitive
22617            // ident. The opening `[` must follow immediately. v7.39 (read01
22618            // round 105) — the body moved out-of-line so its `Vec`/loop locals
22619            // leave parse_atom's frame (which sits on the nesting-budget cycle).
22620            Token::Ident(s) | Token::QuotedIdent(s)
22621                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22622            {
22623                self.parse_array_literal_body()
22624            }
22625            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22626            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22627            // We special-case before the generic ident dispatch so
22628            // the AGAINST clause never reaches the function-call
22629            // loop (which would mis-read `(cols) AGAINST` as a
22630            // call with no trailing modifier). The shape is
22631            // rewritten to a Boolean OR over per-column
22632            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22633            // term)` so the existing FTS evaluator handles
22634            // semantics — the fulltext-GIN built at CREATE TABLE
22635            // time is currently a "real index that survives dump
22636            // round-trip"; the planner hook that actually uses
22637            // it for posting-list intersection lands in a later
22638            // sub-phase (Phase 2.2b) without touching this surface.
22639            Token::Ident(s) | Token::QuotedIdent(s)
22640                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22641            {
22642                self.parse_match_against_atom()
22643            }
22644            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22645            // v7.37.43-T4 — PG-unreserved keywords are legal column /
22646            // alias names in expression context too. `release` appears
22647            // in sentori `0003_partition_events.sql` as both a column
22648            // reference (SELECT … release …) and an INSERT column list
22649            // entry. Mirrors `expect_ident_like`'s expansion of the
22650            // identifier set.
22651            other if unreserved_keyword_text(&other).is_some() => {
22652                let s = unreserved_keyword_text(&other).unwrap();
22653                self.finish_ident_atom(s)
22654            }
22655            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22656            // only inside `SET` before, so `SELECT @@autocommit` — which
22657            // every MySQL connector asks at handshake — was a parse error.
22658            // MariaDB accepts the bare, `@@session.` and `@@global.`
22659            // spellings alike and answers from the session's own value.
22660            Token::SessionVar(v) => {
22661                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22662                // has nothing to do with a `@@` engine setting: its own
22663                // per-session namespace, and an unset one reads NULL instead
22664                // of raising. Stripping every `@` (as this did) made `@x` and
22665                // `@@x` the same node, so `SELECT @x` answered "Unknown
22666                // system variable".
22667                Ok(variable_ref_atom(&v))
22668            }
22669            other => Err(ParseError {
22670                message: format!("unexpected token {other:?} in expression"),
22671                token_pos: tok_pos,
22672            }),
22673        }
22674        // After parsing the atom, fold any postfix `::vector` casts.
22675        .and_then(|atom| self.finish_postfix_casts(atom))
22676    }
22677
22678    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22679    /// Both bind tighter than any binary op.
22680    /// Shared cast-target parser for postfix `::TYPE` and the
22681    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22682    /// If the next tokens are `( N )`, consume them and return the canonical
22683    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22684    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22685    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22686        if !matches!(self.peek(), Token::LParen) {
22687            return None;
22688        }
22689        self.advance(); // (
22690        let n = match self.advance() {
22691            Token::Integer(n) => n,
22692            _ => return Some(base.to_string()), // malformed → drop precision
22693        };
22694        if matches!(self.peek(), Token::RParen) {
22695            self.advance();
22696        }
22697        Some(alloc::format!("{base}({n})"))
22698    }
22699
22700    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22701        // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22702        // schema-qualifies every cast target, and `pg_catalog.X` names
22703        // exactly the builtin type X. Consume the qualifier and let
22704        // the ordinary target parse decide.
22705        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22706            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22707        {
22708            self.advance();
22709            self.advance();
22710        }
22711        let target = match self.advance() {
22712            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22713                "int" | "integer" | "int4" => {
22714                    if matches!(self.peek(), Token::LBracket)
22715                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22716                    {
22717                        self.advance();
22718                        self.advance();
22719                        CastTarget::IntArray
22720                    } else {
22721                        CastTarget::Int
22722                    }
22723                }
22724                "bigint" | "int8" => {
22725                    if matches!(self.peek(), Token::LBracket)
22726                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22727                    {
22728                        self.advance();
22729                        self.advance();
22730                        CastTarget::BigIntArray
22731                    } else {
22732                        CastTarget::BigInt
22733                    }
22734                }
22735                "float" | "double" => CastTarget::Float,
22736                "text" => {
22737                    // v7.10.11 — `::TEXT[]` widens to TextArray.
22738                    if matches!(self.peek(), Token::LBracket)
22739                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22740                    {
22741                        self.advance();
22742                        self.advance();
22743                        CastTarget::TextArray
22744                    } else {
22745                        CastTarget::Text
22746                    }
22747                }
22748                "bool" | "boolean" => CastTarget::Bool,
22749                "vector" => CastTarget::Vector,
22750                "date" => CastTarget::Date,
22751                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22752                // seconds precision through the Named path (the engine rounds
22753                // the sub-second field); bare `::timestamp` keeps the fast arm.
22754                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22755                    Some(named) => CastTarget::Named(named),
22756                    None => CastTarget::Timestamp,
22757                },
22758                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22759                    Some(named) => CastTarget::Named(named),
22760                    None => CastTarget::Timestamptz,
22761                },
22762                "interval" => CastTarget::Interval,
22763                "json" => CastTarget::Json,
22764                "jsonb" => CastTarget::Jsonb,
22765                // v7.39 (round 694) — these have dedicated CastTarget
22766                // variants, so they never reached the postfix `[]` handling
22767                // further down and `::regtype[]` was a SYNTAX error at the
22768                // `]`. PG has an array type for every scalar; take the
22769                // suffix here and hand the canonical `<ty>_array` name to
22770                // the engine, the same shape every other array cast uses.
22771                "regtype" if self.peek_postfix_array_brackets() => {
22772                    self.advance();
22773                    self.advance();
22774                    CastTarget::Named(alloc::string::String::from("regtype_array"))
22775                }
22776                "regclass" if self.peek_postfix_array_brackets() => {
22777                    self.advance();
22778                    self.advance();
22779                    CastTarget::Named(alloc::string::String::from("regclass_array"))
22780                }
22781                "regtype" => CastTarget::RegType,
22782                "regclass" => CastTarget::RegClass,
22783                // v7.12.0 — `::tsvector` / `::tsquery`.
22784                // Engine decodes the LHS text via the PG
22785                // external form parser.
22786                // v7.39 (round 352, M8) — MySQL's own cast targets.
22787                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22788                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22789                // such type, so they are taken only in that dialect and
22790                // fall through to the "type does not exist" arm otherwise.
22791                "signed" | "unsigned" if self.mysql_dialect => {
22792                    if matches!(self.peek(), Token::Ident(k)
22793                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22794                    {
22795                        self.advance();
22796                    }
22797                    CastTarget::Named(s.to_ascii_lowercase())
22798                }
22799                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22800                // in MySQL: MariaDB answers '123' where the SQL-standard
22801                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22802                // Truncating a number to its first digit is a wrong answer
22803                // with no error, so the MySQL session gets MySQL's reading.
22804                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22805                    CastTarget::Text
22806                }
22807                "tsvector" => CastTarget::TsVector,
22808                "tsquery" => CastTarget::TsQuery,
22809                // v7.17.0 — `::uuid`. Engine decodes the LHS
22810                // text via `spg_storage::parse_uuid_str`.
22811                "uuid" => CastTarget::Uuid,
22812                // v7.18 — `::bytea`. Engine decodes the LHS
22813                // text via the PG hex form (`'\xdeadbeef'`)
22814                // or escape form (`'\\x05\\x00'`). Closes
22815                // mailrs D-pre #3 reverse-acceptance gap.
22816                "bytea" => CastTarget::Bytea,
22817                // v7.37.5 ship triage — generic typed-cast escape.
22818                // Anything the long-tail PG type ident table knows
22819                // about(network/bit/geometry/multirange/etc.)flows
22820                // through `CastTarget::Named(canonical)`; the engine
22821                // resolves via `column_type_to_data_type` and dispatches
22822                // through the typed `coerce_value` path. Truly
22823                // unrecognised idents still hit the error arm below
22824                // because the engine rejects them.
22825                other => {
22826                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22827                    // `::varchar(255)`, etc. Capture into the canonical
22828                    // `name(p,s)` form so `type_name_to_data_type` can
22829                    // reconstruct the `DataType::Numeric { precision,
22830                    // scale }` (and similar param-carrying types).
22831                    let mut name = other.to_string();
22832                    // v7.39 (round 281) — `::bit varying(3)` is two
22833                    // words; fold the tail in so the typmod reaches the
22834                    // type resolver instead of tripping the parser.
22835                    if name.eq_ignore_ascii_case("bit")
22836                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22837                    {
22838                        self.advance();
22839                        name = alloc::string::String::from("varbit");
22840                    }
22841                    // v7.39 (round 613) — `::character varying` is the same
22842                    // two-word shape and had no fold, so the `varying` was
22843                    // left behind and the cast became a bare `character`,
22844                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22845                    // `a` where PG answers `ab`. Silently, and for a spelling
22846                    // pg_dump writes.
22847                    if name.eq_ignore_ascii_case("character")
22848                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22849                    {
22850                        self.advance();
22851                        name = alloc::string::String::from("varchar");
22852                    }
22853                    if matches!(self.peek(), Token::LParen) {
22854                        let mut buf = alloc::string::String::from("(");
22855                        let mut depth = 0usize;
22856                        loop {
22857                            match self.advance() {
22858                                Token::LParen => {
22859                                    depth += 1;
22860                                    if depth > 1 {
22861                                        buf.push('(');
22862                                    }
22863                                }
22864                                Token::RParen => {
22865                                    depth -= 1;
22866                                    if depth == 0 {
22867                                        buf.push(')');
22868                                        break;
22869                                    }
22870                                    buf.push(')');
22871                                }
22872                                Token::Comma => buf.push(','),
22873                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22874                                // v7.39 (round 273) — a minus used to fall
22875                                // into the catch-all below and vanish, so
22876                                // `::numeric(10,-2)` reached the engine as
22877                                // the text `numeric(10,2)` and silently
22878                                // rounded to two DECIMALS instead of to
22879                                // hundreds. A dropped token is not a
22880                                // no-op when it carries a sign.
22881                                Token::Minus => buf.push('-'),
22882                                Token::Eof => break,
22883                                _ => {}
22884                            }
22885                        }
22886                        name.push_str(&buf);
22887                    }
22888                    // Optional postfix `[]` widens to the array form —
22889                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22890                    // The engine's `type_name_to_data_type` recognises
22891                    // the canonical `<ty>_array` form.
22892                    if matches!(self.peek(), Token::LBracket)
22893                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22894                    {
22895                        self.advance();
22896                        self.advance();
22897                        name.push_str("_array");
22898                    }
22899                    CastTarget::Named(name)
22900                }
22901            },
22902            Token::Interval => CastTarget::Interval,
22903            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22904            // "char" (oid 18, SPG Char1 — distinct from bare `char`
22905            // = char(1)); other quoted names resolve like idents.
22906            Token::QuotedIdent(q) => {
22907                if q.eq_ignore_ascii_case("char") {
22908                    CastTarget::Named("char1".into())
22909                } else {
22910                    CastTarget::Named(q.to_ascii_lowercase())
22911                }
22912            }
22913            other => {
22914                return Err(ParseError {
22915                    message: format!("expected type ident after `::`, got {other:?}"),
22916                    token_pos: self.consumed_pos(),
22917                });
22918            }
22919        };
22920        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22921        // target to its array sibling. Closed-enum arms (Bool /
22922        // SmallInt / Numeric / Float / Date / …) didn't carry the
22923        // explicit widening that Text / Int / BigInt did, so
22924        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22925        // error. The widening here mirrors the per-arm Text /
22926        // Int / BigInt logic above + folds the new ζ-A first-class
22927        // types through `CastTarget::Named("<ty>_array")`.
22928        if matches!(self.peek(), Token::LBracket)
22929            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22930        {
22931            let widened = match &target {
22932                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22933                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22934                // v7.39 (round 326, V43) — the two temporal types stay
22935                // distinct. Both used to widen to `timestamptz_array`, so
22936                // `::timestamp[]` named the wrong target in its own error
22937                // message and lost the zone-less identity on the way.
22938                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22939                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22940                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22941                CastTarget::Json | CastTarget::Jsonb => {
22942                    Some(CastTarget::Named("jsonb_array".to_string()))
22943                }
22944                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22945                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22946                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22947                CastTarget::Named(name) => {
22948                    let mut a = name.clone();
22949                    a.push_str("_array");
22950                    Some(CastTarget::Named(a))
22951                }
22952                // Int / BigInt / Text / Vector / TsVector / TsQuery /
22953                // RegType / RegClass / TextArray / IntArray /
22954                // BigIntArray already finalised — leave as is.
22955                _ => None,
22956            };
22957            if let Some(w) = widened {
22958                self.advance();
22959                self.advance();
22960                return Ok(w);
22961            }
22962        }
22963        Ok(target)
22964    }
22965
22966    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22967        loop {
22968            // v7.38 (read01, T9) — composite field access `(expr).field`.
22969            // A bare `a.b` is consumed as a qualified column inside the ident
22970            // atom, so a Dot only survives to this postfix position when the
22971            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22972            // `.*` whole-row expansion is not handled here (projection-level).
22973            if matches!(self.peek(), Token::Dot)
22974                && matches!(
22975                    self.tokens.get(self.pos + 1),
22976                    Some(Token::Ident(_) | Token::QuotedIdent(_))
22977                )
22978            {
22979                self.advance(); // .
22980                let field = match self.advance() {
22981                    Token::Ident(s) | Token::QuotedIdent(s) => s,
22982                    other => {
22983                        return Err(
22984                            self.err(format!("expected a field name after '.', got {other:?}"))
22985                        );
22986                    }
22987                };
22988                expr = Expr::FieldAccess {
22989                    base: Box::new(expr),
22990                    field,
22991                };
22992                continue;
22993            }
22994            if matches!(self.peek(), Token::DoubleColon) {
22995                self.advance();
22996                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22997                // target set to include INTERVAL (reserved Token),
22998                // TIMESTAMPTZ, and PG catalog regtype / regclass.
22999                // mailrs follow-up H3a + H3b.
23000                let target = self.parse_cast_target()?;
23001                expr = Expr::Cast {
23002                    expr: Box::new(expr),
23003                    target,
23004                };
23005                continue;
23006            }
23007            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
23008            // returns NULL for out-of-range. Multiple subscripts
23009            // chain: `a[i][j]` parses left-to-right.
23010            if matches!(self.peek(), Token::LBracket) {
23011                self.advance();
23012                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
23013                // bare index stays a subscript.
23014                let lo = if matches!(self.peek(), Token::Colon) {
23015                    None
23016                } else {
23017                    Some(self.parse_expr(0)?)
23018                };
23019                if matches!(self.peek(), Token::Colon) {
23020                    self.advance();
23021                    let hi = if matches!(self.peek(), Token::RBracket) {
23022                        None
23023                    } else {
23024                        Some(Box::new(self.parse_expr(0)?))
23025                    };
23026                    if !matches!(self.peek(), Token::RBracket) {
23027                        return Err(self.err(alloc::format!(
23028                            "expected ']' after array slice, got {:?}",
23029                            self.peek()
23030                        )));
23031                    }
23032                    self.advance();
23033                    expr = Expr::ArraySlice {
23034                        target: Box::new(expr),
23035                        lo: lo.map(Box::new),
23036                        hi,
23037                    };
23038                    continue;
23039                }
23040                let index = lo.expect("non-colon branch parsed an index");
23041                if !matches!(self.peek(), Token::RBracket) {
23042                    return Err(self.err(alloc::format!(
23043                        "expected ']' after array index, got {:?}",
23044                        self.peek()
23045                    )));
23046                }
23047                self.advance();
23048                expr = Expr::ArraySubscript {
23049                    target: Box::new(expr),
23050                    index: Box::new(index),
23051                };
23052                continue;
23053            }
23054            // `expr AT TIME ZONE zone` — lowers to PG's own function
23055            // form timezone(zone, expr); the scalar implements the
23056            // offset shift (named zones error there — no tzdata).
23057            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
23058                && matches!(self.tokens.get(self.pos + 1),
23059                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
23060                && matches!(self.tokens.get(self.pos + 2),
23061                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
23062            {
23063                self.advance(); // AT
23064                self.advance(); // TIME
23065                self.advance(); // ZONE
23066                // Zone at comparison precedence so AND/OR stay out.
23067                let zone = self.parse_expr(6)?;
23068                expr = Expr::FunctionCall {
23069                    name: "timezone".to_string(),
23070                    args: alloc::vec![zone, expr],
23071                };
23072                continue;
23073            }
23074            // `expr COLLATE "name"` — SPG's single text ordering IS
23075            // byte order, i.e. the C collation. The byte-order
23076            // spellings absorb as no-ops; a locale collation would
23077            // silently sort differently from PG, so it errors
23078            // honestly instead.
23079            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
23080                self.advance();
23081                let mut cname = match self.advance() {
23082                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23083                    other => {
23084                        return Err(self.err(alloc::format!(
23085                            "expected collation name after COLLATE, got {other:?}"
23086                        )));
23087                    }
23088                };
23089                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
23090                // is how `pg_dump` writes the default one:
23091                // `… COLLATE pg_catalog.default`. Reading a single token
23092                // left the SCHEMA as the name, so the clause was refused
23093                // as an unsupported locale collation and no dump ran.
23094                if matches!(self.peek(), Token::Dot) {
23095                    // v7.39.2 — the qualifier is DROPPED (SPG is single
23096                    // schema) but it is checked first. PostgreSQL 18.6
23097                    // answers `schema "nosuch_schema" does not exist` for
23098                    // one it has never heard of, and dropping it unread
23099                    // meant `COLLATE nosuch_schema."C"` succeeded here —
23100                    // a name that names nothing, accepted.
23101                    let schema = cname.to_ascii_lowercase();
23102                    if !matches!(
23103                        schema.as_str(),
23104                        "pg_catalog" | "public" | "information_schema"
23105                    ) {
23106                        return Err(self.err(alloc::format!("schema \"{cname}\" does not exist")));
23107                    }
23108                    self.advance();
23109                    cname = match self.advance() {
23110                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23111                        // `default` lexes as a KEYWORD, and it is the name
23112                        // pg_dump writes — the same trap round 535 hit with
23113                        // TABLE / INDEX / FULL.
23114                        Token::Default => alloc::string::String::from("default"),
23115                        other => {
23116                            return Err(self.err(alloc::format!(
23117                                "expected collation name after COLLATE, got {other:?}"
23118                            )));
23119                        }
23120                    };
23121                }
23122                let lc = cname.to_ascii_lowercase();
23123                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
23124                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
23125                // family / `binary`) forces byte-wise, which is exactly
23126                // what `BINARY expr` does — lower onto that so every fold
23127                // site (comparison, LIKE, ORDER BY) suppresses via
23128                // `is_binary_coerced`. A `_ci` family override folds, and
23129                // under the MySQL dialect the default already folds, so it
23130                // absorbs as a no-op; likewise the C / byte-order spellings.
23131                // v7.39.2 — against MySQL's own list, not against the
23132                // shape of the name. `nosuch_bin` took this shortcut and
23133                // became a BINARY cast; `nosuch_ci` took the one below
23134                // and was absorbed as a no-op. Either way the client
23135                // named a collation that does not exist and was told
23136                // nothing. An unknown name now falls through to the
23137                // node, and the engine refuses it.
23138                let real = crate::charset::is_mysql_collation(&lc);
23139                if self.mysql_dialect && real && (lc.ends_with("_bin") || lc == "binary") {
23140                    expr = Expr::Cast {
23141                        expr: alloc::boxed::Box::new(expr),
23142                        target: CastTarget::Named("binary".to_string()),
23143                    };
23144                    continue;
23145                }
23146                let mysql_ci = self.mysql_dialect
23147                    && ((real && lc.ends_with("_ci"))
23148                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
23149                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
23150                // goes to the lowering channel, the byte-order spellings
23151                // included. Round 691 recorded only the names the old
23152                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
23153                // absorbed as a no-op — and once a column could declare a
23154                // collation, absorbing the clause meant the COLUMN's
23155                // collation won where the query had asked for bytes.
23156                if self.in_order_by_key && !mysql_ci {
23157                    self.order_key_collation = Some(cname);
23158                    continue;
23159                }
23160                // v7.39.2 — the clause becomes a NODE rather than being
23161                // refused or absorbed.
23162                //
23163                // What stood here refused the locale names and SILENTLY
23164                // DROPPED the byte-order ones, so `'a' COLLATE "C" < 'B'`
23165                // answered `t` where PostgreSQL 18.6 answers `f`: the one
23166                // family it let through is the one where dropping it
23167                // changes the answer. Absorbing is only correct when the
23168                // collation asked for is the one the comparison would use
23169                // anyway, and that depends on the DATABASE — which the
23170                // parser cannot see. So it rides along and the engine,
23171                // which can, decides.
23172                //
23173                // `collate_derive` already modelled `Explicit(name)` and
23174                // had no way to be handed one.
23175                // v7.39.2 — a MySQL spelling does not exist on the
23176                // PostgreSQL wire, and THIS is where the wire is known.
23177                //
23178                // The check lived in the evaluator first and asked
23179                // `ctx.mysql_dialect`, which the INSERT path builds as a
23180                // hard-coded `false` — so `INSERT … VALUES (_utf8mb4'x')`
23181                // in a MySQL session was refused for a collation that
23182                // does not exist on a wire it was not on. Making that
23183                // context truthful would change INSERT-time evaluation
23184                // in other ways as a side effect; the parser already
23185                // gates the introducer on the same flag and is the
23186                // honest place to ask.
23187                if !self.mysql_dialect
23188                    && (lc.ends_with("_ci")
23189                        || lc.ends_with("_cs")
23190                        || lc.ends_with("_bin")
23191                        || lc == "binary"
23192                        || matches!(lc.as_str(), "case_insensitive" | "nocase"))
23193                {
23194                    return Err(self.err(alloc::format!(
23195                        "collation \"{cname}\" for encoding \"UTF8\" does not exist"
23196                    )));
23197                }
23198                // v7.39.3 — the node is built for EVERY name, `_ci`
23199                // included.
23200                //
23201                // A MySQL `_ci` spelling used to be absorbed here on the
23202                // reasoning that a MySQL session folds anyway, so the
23203                // clause asked for what it would have got. That stopped
23204                // being true when the fold learned to read the session's
23205                // collation NAME: under `SET NAMES utf8mb4 COLLATE
23206                // utf8mb4_bin`, `'AB' COLLATE utf8mb4_general_ci = 'ab'`
23207                // is 1 on MySQL 9.7.2 and was 0 here, because the clause
23208                // that would have made it 1 had been dropped in the
23209                // parser. Absorbing is only ever correct when the
23210                // collation asked for is the one the comparison would use
23211                // anyway, and the parser cannot know that — the same
23212                // reasoning already written above for the byte-order
23213                // spellings, applied to the family it had exempted.
23214                expr = Expr::Collate {
23215                    expr: alloc::boxed::Box::new(expr),
23216                    collation: cname,
23217                };
23218                continue;
23219            }
23220            return Ok(expr);
23221        }
23222    }
23223
23224    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
23225    /// the first token that is not one. Schema qualifiers collapse to the
23226    /// last part, which is what every other name path here does (SPG is
23227    /// single-schema).
23228    fn take_comma_separated_names(&mut self) -> Vec<String> {
23229        let mut out = Vec::new();
23230        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
23231            self.advance();
23232            let mut last = n;
23233            while matches!(self.peek(), Token::Dot) {
23234                self.advance();
23235                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
23236                    last = t;
23237                }
23238            }
23239            out.push(last);
23240            if matches!(self.peek(), Token::Comma) {
23241                self.advance();
23242            } else {
23243                break;
23244            }
23245        }
23246        out
23247    }
23248
23249    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
23250    ///
23251    /// The general cast-target path tests this inline; the types with their
23252    /// own `CastTarget` variant need it as a guard on their match arm,
23253    /// which is what this exists for.
23254    fn peek_postfix_array_brackets(&self) -> bool {
23255        matches!(self.peek(), Token::LBracket)
23256            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23257    }
23258
23259    /// Parse the operator tail after a `(a, b, …)` row constructor
23260    /// and expand at parse time. `=` is the conjunction of element
23261    /// equalities; `<>` its negation; the order operators expand
23262    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
23263    /// equalities. Anything else (a bare row value, a subquery
23264    /// RHS) errors honestly — SPG has no composite runtime value.
23265    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
23266        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
23267            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
23268                lhs: Box::new(l.clone()),
23269                op: BinOp::Eq,
23270                rhs: Box::new(r.clone()),
23271            });
23272            let first = it.next().expect("row has at least two elements");
23273            it.fold(first, |acc, e| Expr::Binary {
23274                lhs: Box::new(acc),
23275                op: BinOp::And,
23276                rhs: Box::new(e),
23277            })
23278        }
23279        // Lexicographic (a,b) OP (c,d):
23280        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
23281        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
23282            if lhs.len() == 1 {
23283                return Expr::Binary {
23284                    lhs: Box::new(lhs[0].clone()),
23285                    op: last,
23286                    rhs: Box::new(rhs[0].clone()),
23287                };
23288            }
23289            let head_strict = Expr::Binary {
23290                lhs: Box::new(lhs[0].clone()),
23291                op: strict,
23292                rhs: Box::new(rhs[0].clone()),
23293            };
23294            let head_eq = Expr::Binary {
23295                lhs: Box::new(lhs[0].clone()),
23296                op: BinOp::Eq,
23297                rhs: Box::new(rhs[0].clone()),
23298            };
23299            Expr::Binary {
23300                lhs: Box::new(head_strict),
23301                op: BinOp::Or,
23302                rhs: Box::new(Expr::Binary {
23303                    lhs: Box::new(head_eq),
23304                    op: BinOp::And,
23305                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
23306                }),
23307            }
23308        }
23309        let negated_in = if matches!(self.peek(), Token::Not)
23310            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
23311        {
23312            self.advance();
23313            true
23314        } else {
23315            false
23316        };
23317        if matches!(self.peek(), Token::In) {
23318            self.advance();
23319            if !matches!(self.peek(), Token::LParen) {
23320                return Err(self.err(alloc::format!(
23321                    "expected '(' after row IN, got {:?}",
23322                    self.peek()
23323                )));
23324            }
23325            self.advance();
23326            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
23327            // not a list of literal rows. Row-vs-list decomposes to
23328            // OR-of-AND above, but the subquery's rows are only known at
23329            // runtime, so keep it as a RowInSubquery node.
23330            if matches!(self.peek(), Token::Select) {
23331                let inner = self.parse_select_stmt()?;
23332                if !matches!(self.peek(), Token::RParen) {
23333                    return Err(self.err(alloc::format!(
23334                        "expected ')' after row IN-subquery, got {:?}",
23335                        self.peek()
23336                    )));
23337                }
23338                self.advance();
23339                let Statement::Select(s) = inner else {
23340                    unreachable!("parse_select_stmt always returns Statement::Select")
23341                };
23342                return Ok(Expr::RowInSubquery {
23343                    row,
23344                    subquery: Box::new(s),
23345                    negated: negated_in,
23346                });
23347            }
23348            let mut alternatives: Vec<Expr> = Vec::new();
23349            loop {
23350                // Optional ROW keyword before the paren row.
23351                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23352                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23353                {
23354                    self.advance();
23355                }
23356                if !matches!(self.peek(), Token::LParen) {
23357                    return Err(self.err(alloc::format!(
23358                        "expected '(' to open a row inside IN, got {:?}",
23359                        self.peek()
23360                    )));
23361                }
23362                self.advance();
23363                let mut rhs = alloc::vec![self.parse_expr(0)?];
23364                while matches!(self.peek(), Token::Comma) {
23365                    self.advance();
23366                    rhs.push(self.parse_expr(0)?);
23367                }
23368                if !matches!(self.peek(), Token::RParen) {
23369                    return Err(self.err(alloc::format!(
23370                        "expected ')' after row inside IN, got {:?}",
23371                        self.peek()
23372                    )));
23373                }
23374                self.advance();
23375                if rhs.len() != row.len() {
23376                    return Err(self.err(alloc::format!(
23377                        "row IN arity mismatch: left has {}, right has {}",
23378                        row.len(),
23379                        rhs.len()
23380                    )));
23381                }
23382                alternatives.push(row_eq(&row, &rhs));
23383                if matches!(self.peek(), Token::Comma) {
23384                    self.advance();
23385                    continue;
23386                }
23387                break;
23388            }
23389            if !matches!(self.peek(), Token::RParen) {
23390                return Err(self.err(alloc::format!(
23391                    "expected ')' to close row IN list, got {:?}",
23392                    self.peek()
23393                )));
23394            }
23395            self.advance();
23396            let mut it = alternatives.into_iter();
23397            let first = it.next().expect("IN list has at least one row");
23398            let combined = it.fold(first, |acc, e| Expr::Binary {
23399                lhs: Box::new(acc),
23400                op: BinOp::Or,
23401                rhs: Box::new(e),
23402            });
23403            return Ok(maybe_not(combined, negated_in));
23404        }
23405        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23406        // two periods share at least one time point. Each pair is
23407        // normalised with least/greatest (PG accepts the endpoints
23408        // in either order), then lowered to the standard
23409        // `start1 < end2 AND start2 < end1` form.
23410        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23411            if row.len() != 2 {
23412                return Err(self.err(alloc::format!(
23413                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
23414                    row.len()
23415                )));
23416            }
23417            self.advance();
23418            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23419                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23420            {
23421                self.advance();
23422            }
23423            if !matches!(self.peek(), Token::LParen) {
23424                return Err(self.err(alloc::format!(
23425                    "expected '(' after OVERLAPS, got {:?}",
23426                    self.peek()
23427                )));
23428            }
23429            self.advance();
23430            let r0 = self.parse_expr(0)?;
23431            if !matches!(self.peek(), Token::Comma) {
23432                return Err(self.err(alloc::format!(
23433                    "OVERLAPS needs (start, end) on the right, got {:?}",
23434                    self.peek()
23435                )));
23436            }
23437            self.advance();
23438            let r1 = self.parse_expr(0)?;
23439            if !matches!(self.peek(), Token::RParen) {
23440                return Err(self.err(alloc::format!(
23441                    "expected ')' after OVERLAPS pair, got {:?}",
23442                    self.peek()
23443                )));
23444            }
23445            self.advance();
23446            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
23447                name: String::from(name),
23448                args: alloc::vec![a.clone(), b.clone()],
23449            };
23450            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
23451                lhs: Box::new(lhs),
23452                op: BinOp::Lt,
23453                rhs: Box::new(rhs),
23454            };
23455            return Ok(Expr::Binary {
23456                lhs: Box::new(lt(
23457                    pair_fn("least", &row[0], &row[1]),
23458                    pair_fn("greatest", &r0, &r1),
23459                )),
23460                op: BinOp::And,
23461                rhs: Box::new(lt(
23462                    pair_fn("least", &r0, &r1),
23463                    pair_fn("greatest", &row[0], &row[1]),
23464                )),
23465            });
23466        }
23467        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
23468        // PG, `IS NULL` is true only when EVERY field is NULL, and
23469        // `IS NOT NULL` is true only when every field is non-NULL — the
23470        // latter is NOT the negation of the former (a mixed row is
23471        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
23472        // which reproduces exactly that all-fields semantics.
23473        if matches!(self.peek(), Token::Is) {
23474            self.advance();
23475            let negated = if matches!(self.peek(), Token::Not) {
23476                self.advance();
23477                true
23478            } else {
23479                false
23480            };
23481            if !matches!(self.peek(), Token::Null) {
23482                return Err(self.err(alloc::format!(
23483                    "expected NULL after row IS [NOT], got {:?}",
23484                    self.peek()
23485                )));
23486            }
23487            self.advance();
23488            let mut it = row.iter().map(|e| Expr::IsNull {
23489                expr: Box::new(e.clone()),
23490                negated,
23491            });
23492            let first = it.next().expect("row has at least two elements");
23493            return Ok(it.fold(first, |acc, e| Expr::Binary {
23494                lhs: Box::new(acc),
23495                op: BinOp::And,
23496                rhs: Box::new(e),
23497            }));
23498        }
23499        let op = match self.peek() {
23500            Token::Eq => BinOp::Eq,
23501            Token::NotEq => BinOp::NotEq,
23502            Token::Lt => BinOp::Lt,
23503            Token::LtEq => BinOp::LtEq,
23504            Token::Gt => BinOp::Gt,
23505            Token::GtEq => BinOp::GtEq,
23506            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
23507            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
23508            // constructor value, identical to the `ROW(a, b, …)` keyword form:
23509            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
23510            // (`::text`, `.field`) applies at the caller just as it does for the
23511            // ROW(...) node. All the comparison / predicate forms returned above.
23512            _ => {
23513                return Ok(Expr::FunctionCall {
23514                    name: String::from("row"),
23515                    args: row,
23516                });
23517            }
23518        };
23519        self.advance();
23520        // Optional ROW keyword before the paren row.
23521        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23522            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23523        {
23524            self.advance();
23525        }
23526        if !matches!(self.peek(), Token::LParen) {
23527            return Err(self.err(alloc::format!(
23528                "expected '(' to open the right-hand row, got {:?}",
23529                self.peek()
23530            )));
23531        }
23532        self.advance();
23533        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
23534        // subquery. Kept as a node (the subquery's row is a runtime value);
23535        // the literal-RHS form below still decomposes at parse time.
23536        if matches!(self.peek(), Token::Select) {
23537            let inner = self.parse_select_stmt()?;
23538            if !matches!(self.peek(), Token::RParen) {
23539                return Err(self.err(alloc::format!(
23540                    "expected ')' after row comparison subquery, got {:?}",
23541                    self.peek()
23542                )));
23543            }
23544            self.advance();
23545            let Statement::Select(s) = inner else {
23546                unreachable!("parse_select_stmt always returns Statement::Select")
23547            };
23548            return Ok(Expr::RowCmpSubquery {
23549                row,
23550                op,
23551                subquery: Box::new(s),
23552            });
23553        }
23554        let mut rhs = alloc::vec![self.parse_expr(0)?];
23555        while matches!(self.peek(), Token::Comma) {
23556            self.advance();
23557            rhs.push(self.parse_expr(0)?);
23558        }
23559        if !matches!(self.peek(), Token::RParen) {
23560            return Err(self.err(alloc::format!(
23561                "expected ')' after right-hand row, got {:?}",
23562                self.peek()
23563            )));
23564        }
23565        self.advance();
23566        if rhs.len() != row.len() {
23567            // v7.39 (round 239) — PG's wording (42601).
23568            return Err(self.err("unequal number of entries in row expressions".to_string()));
23569        }
23570        Ok(match op {
23571            BinOp::Eq => row_eq(&row, &rhs),
23572            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
23573            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
23574            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
23575            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
23576            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
23577            _ => unreachable!("op restricted above"),
23578        })
23579    }
23580
23581    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
23582    /// escape character becomes the matcher's default backslash:
23583    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
23584    /// → the char itself, and any pre-existing backslash escapes
23585    /// itself so it stays literal. Both operands must be string
23586    /// literals — a runtime pattern would need matcher support.
23587    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
23588        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
23589            (&pattern, &esc)
23590        else {
23591            return Err(
23592                "LIKE ... ESCAPE requires string-literal pattern and escape \
23593                 (runtime escape characters are not supported yet)"
23594                    .into(),
23595            );
23596        };
23597        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
23598        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
23599        // multi-character escape is an error.
23600        let esc_ch: Option<char> = {
23601            let mut ch_iter = e.chars();
23602            match (ch_iter.next(), ch_iter.next()) {
23603                (Some(c), None) => Some(c),
23604                (None, _) => None,
23605                (Some(_), Some(_)) => {
23606                    return Err(alloc::format!(
23607                        "ESCAPE must be a single character, got {e:?}"
23608                    ));
23609                }
23610            }
23611        };
23612        let mut out = String::with_capacity(p.len() + 4);
23613        let mut chars = p.chars();
23614        while let Some(c) = chars.next() {
23615            if Some(c) == esc_ch {
23616                match chars.next() {
23617                    // Escaped wildcard / escaped escape → keep the
23618                    // next char literal via backslash.
23619                    Some(next) => {
23620                        out.push('\\');
23621                        out.push(next);
23622                    }
23623                    None => {
23624                        return Err("LIKE pattern ends with the escape character".into());
23625                    }
23626                }
23627            } else if c == '\\' && esc_ch != Some('\\') {
23628                // A raw backslash is literal under a custom (or absent) escape
23629                // — escape it for the backslash-based matcher.
23630                out.push('\\');
23631                out.push('\\');
23632            } else {
23633                out.push(c);
23634            }
23635        }
23636        Ok(Expr::Literal(Literal::String(out)))
23637    }
23638
23639    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
23640    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
23641    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
23642    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
23643    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
23644    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
23645    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
23646    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
23647    /// array expression errors honestly rather than silently mismatching.
23648    fn try_like_any_all(
23649        &mut self,
23650        base: &Expr,
23651        negated: bool,
23652        case_insensitive: bool,
23653    ) -> Result<Option<Expr>, ParseError> {
23654        let is_any = match self.peek() {
23655            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
23656            Token::Ident(s)
23657                if s.eq_ignore_ascii_case("any")
23658                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
23659            {
23660                true
23661            }
23662            _ => return Ok(None),
23663        };
23664        self.advance(); // ANY / ALL
23665        self.advance(); // '('
23666        let arr = self.parse_expr(0)?;
23667        if !matches!(self.peek(), Token::RParen) {
23668            return Err(self.err(format!(
23669                "expected ')' after LIKE {} argument, got {:?}",
23670                if is_any { "ANY" } else { "ALL" },
23671                self.peek()
23672            )));
23673        }
23674        self.advance(); // ')'
23675        let Expr::Array(items) = arr else {
23676            return Err(self.err(
23677                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23678            ));
23679        };
23680        let mut clauses = items.into_iter().map(|p| Expr::Like {
23681            expr: Box::new(base.clone()),
23682            pattern: Box::new(p),
23683            negated,
23684            case_insensitive,
23685        });
23686        let Some(first) = clauses.next() else {
23687            // ANY(empty) = FALSE, ALL(empty) = TRUE.
23688            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23689        };
23690        let op = if is_any { BinOp::Or } else { BinOp::And };
23691        let combined = clauses.fold(first, |acc, c| Expr::Binary {
23692            lhs: Box::new(acc),
23693            op,
23694            rhs: Box::new(c),
23695        });
23696        Ok(Some(combined))
23697    }
23698
23699    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
23700    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23701    /// `AND` is not swallowed.
23702    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23703        self.advance(); // BETWEEN
23704        // SYMMETRIC — the bounds may arrive in either order; both
23705        // orientations OR together. ASYMMETRIC is the default and
23706        // absorbs as noise.
23707        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23708        {
23709            self.advance();
23710            true
23711        } else {
23712            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23713                self.advance();
23714            }
23715            false
23716        };
23717        let low = self.parse_expr(6)?;
23718        if !matches!(self.peek(), Token::And) {
23719            return Err(self.err(format!(
23720                "expected AND after BETWEEN low bound, got {:?}",
23721                self.peek()
23722            )));
23723        }
23724        self.advance();
23725        let high = self.parse_expr(6)?;
23726        let target = Box::new(expr);
23727        let range = |lo: Expr, hi: Expr| Expr::Binary {
23728            lhs: Box::new(Expr::Binary {
23729                lhs: target.clone(),
23730                op: BinOp::GtEq,
23731                rhs: Box::new(lo),
23732            }),
23733            op: BinOp::And,
23734            rhs: Box::new(Expr::Binary {
23735                lhs: target.clone(),
23736                op: BinOp::LtEq,
23737                rhs: Box::new(hi),
23738            }),
23739        };
23740        let combined = if symmetric {
23741            Expr::Binary {
23742                lhs: Box::new(range(low.clone(), high.clone())),
23743                op: BinOp::Or,
23744                rhs: Box::new(range(high, low)),
23745            }
23746        } else {
23747            range(low, high)
23748        };
23749        Ok(maybe_not(combined, negated))
23750    }
23751
23752    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
23753    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23754    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23755    /// Caller already consumed the leading `WITH` ident.
23756    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23757    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23758    /// self-reference that appears more than once in a single term.
23759    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23760        use crate::ast::{CteBody, SelectStatement};
23761        if !cte.recursive {
23762            return Ok(());
23763        }
23764        let CteBody::Select(body) = &cte.body else {
23765            return Ok(());
23766        };
23767        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23768        // check the anchor and every peer term.
23769        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23770        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23771        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23772            return Err(self.err(String::from(
23773                "ORDER BY in a recursive query is not implemented",
23774            )));
23775        }
23776        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23777            return Err(self.err(String::from(
23778                "LIMIT in a recursive query is not implemented",
23779            )));
23780        }
23781        let self_refs = |s: &SelectStatement| -> usize {
23782            let Some(from) = &s.from else {
23783                return 0;
23784            };
23785            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23786            for j in &from.joins {
23787                if j.table.name.eq_ignore_ascii_case(&cte.name) {
23788                    n += 1;
23789                }
23790            }
23791            n
23792        };
23793        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23794            return Err(self.err(alloc::format!(
23795                "recursive reference to query \"{}\" must not appear more than once",
23796                cte.name
23797            )));
23798        }
23799        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23800        // apply only when the body actually references itself (a non-self-
23801        // referencing CTE under WITH RECURSIVE may use any set-op shape).
23802        let anchor_refs = self_refs(body);
23803        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23804        if anchor_refs > 0 || union_refs {
23805            // Shape: the top level must be UNION [ALL] arms only. A self-ref
23806            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23807            // "does not have the form" error — SPG used to compute a value.
23808            if body.unions.is_empty()
23809                || body.unions.iter().any(|(k, _)| {
23810                    !matches!(
23811                        k,
23812                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23813                    )
23814                })
23815            {
23816                return Err(self.err(alloc::format!(
23817                    "recursive query \"{}\" does not have the form non-recursive-term \
23818                     UNION [ALL] recursive-term",
23819                    cte.name
23820                )));
23821            }
23822            if anchor_refs > 0 {
23823                return Err(self.err(alloc::format!(
23824                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
23825                    cte.name
23826                )));
23827            }
23828        }
23829        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23830        for (_, u) in &body.unions {
23831            if self_refs(u) == 0 {
23832                continue;
23833            }
23834            // The self-reference must not sit on the nullable side of an outer
23835            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23836            if let Some(from) = &u.from {
23837                for (i, j) in from.joins.iter().enumerate() {
23838                    let left_has_self = is_self(&from.primary)
23839                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23840                    let violated = match j.kind {
23841                        crate::ast::JoinKind::Left => is_self(&j.table),
23842                        crate::ast::JoinKind::Right => left_has_self,
23843                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23844                        _ => false,
23845                    };
23846                    if violated {
23847                        return Err(self.err(alloc::format!(
23848                            "recursive reference to query \"{}\" must not appear within an outer join",
23849                            cte.name
23850                        )));
23851                    }
23852                }
23853            }
23854            // No aggregates at the top level of the recursive term (SPG used
23855            // to run them and surface a misleading downstream error).
23856            let mut items_and_having: Vec<&Expr> = Vec::new();
23857            for it in &u.items {
23858                if let crate::ast::SelectItem::Expr { expr, .. } = it {
23859                    items_and_having.push(expr);
23860                }
23861            }
23862            if let Some(h) = &u.having {
23863                items_and_having.push(h);
23864            }
23865            for e in items_and_having {
23866                if expr_has_toplevel_aggregate(e) {
23867                    return Err(self.err(String::from(
23868                        "aggregate functions are not allowed in a recursive query's recursive term",
23869                    )));
23870                }
23871            }
23872        }
23873        // A self-reference inside a sublink expression (EXISTS / IN / scalar
23874        // subquery) anywhere in the body is rejected; a plain FROM derived
23875        // table is legal in PG and untouched here.
23876        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23877        all_terms.extend(body.unions.iter().map(|(_, u)| u));
23878        for term in all_terms {
23879            if select_has_self_ref_in_sublink(term, &cte.name) {
23880                return Err(self.err(alloc::format!(
23881                    "recursive reference to query \"{}\" must not appear within a subquery",
23882                    cte.name
23883                )));
23884            }
23885        }
23886        Ok(())
23887    }
23888
23889    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23890    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23891    /// right after parse so the engine sees a plain recursive CTE with the
23892    /// tracking columns already projected. DEPTH FIRST and CYCLE are
23893    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23894    /// text-rendered rows can't provide, and errors honestly.
23895    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23896        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23897        if cte.search.is_none() && cte.cycle.is_none() {
23898            return Ok(());
23899        }
23900        let cte_name = cte.name.clone();
23901        let col_names = cte.column_overrides.clone();
23902        if col_names.is_empty() {
23903            return Err(
23904                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23905            );
23906        }
23907        let search = cte.search.take();
23908        let cycle = cte.cycle.take();
23909        let mut extra_cols: Vec<String> = Vec::new();
23910        let col_ref = |name: &str| {
23911            Expr::Column(ColumnName {
23912                qualifier: Some(cte_name.clone()),
23913                name: name.to_string(),
23914            })
23915        };
23916        // Position of a SEARCH/CYCLE column within the CTE's column list.
23917        let pos_of = |name: &str| -> Result<usize, ParseError> {
23918            col_names
23919                .iter()
23920                .position(|c| c.eq_ignore_ascii_case(name))
23921                .ok_or_else(|| {
23922                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23923                })
23924        };
23925        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23926            let mut args = Vec::with_capacity(positions.len());
23927            for &p in positions {
23928                match items.get(p) {
23929                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23930                    _ => {
23931                        return Err(self.err(
23932                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
23933                        ));
23934                    }
23935                }
23936            }
23937            Ok(Expr::FunctionCall {
23938                name: "row".into(),
23939                args,
23940            })
23941        };
23942        let CteBody::Select(body) = &mut cte.body else {
23943            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23944        };
23945        if body.unions.is_empty() {
23946            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23947        }
23948        let rec = body.unions.len() - 1; // recursive term = last UNION peer
23949
23950        if let Some(srch) = search {
23951            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23952            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23953            // no typed `record[]`, but element-wise array ORDER BY is correct
23954            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23955            // exactly onto a typed array: DEPTH is the root→node path
23956            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23957            // orders numerically (multi-digit keys included), matching PG.
23958            //
23959            // A multi-column BY would need a record[] to keep the per-node key
23960            // tuple orderable, which SPG can't express — error honestly there
23961            // rather than mis-order.
23962            if srch.by_columns.len() != 1 {
23963                return Err(self.err(
23964                    "SEARCH … BY with multiple columns needs typed record[] ordering \
23965                     SPG doesn't have yet; a single BY column is supported"
23966                        .into(),
23967                ));
23968            }
23969            let key_pos = pos_of(&srch.by_columns[0])?;
23970            let base_key = match body.items.get(key_pos) {
23971                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23972                _ => {
23973                    return Err(
23974                        self.err("SEARCH BY column maps to a non-expression select item".into())
23975                    );
23976                }
23977            };
23978            let rec_key = match body.unions[rec].1.items.get(key_pos) {
23979                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23980                _ => {
23981                    return Err(
23982                        self.err("SEARCH BY column maps to a non-expression select item".into())
23983                    );
23984                }
23985            };
23986            if srch.depth_first {
23987                // base: ARRAY[key]; rec: array_append(cte.set, key).
23988                body.items.push(SelectItem::Expr {
23989                    expr: Expr::Array(alloc::vec![base_key]),
23990                    alias: Some(srch.set_column.clone()),
23991                });
23992                body.unions[rec].1.items.push(SelectItem::Expr {
23993                    expr: Expr::FunctionCall {
23994                        name: "array_append".into(),
23995                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
23996                    },
23997                    alias: Some(srch.set_column.clone()),
23998                });
23999            } else {
24000                // BREADTH: [depth, key]; depth starts at 0 and increments. The
24001                // leading depth element dominates the element-wise comparison,
24002                // so shallower rows sort first, then by key — PG's (depth, key).
24003                body.items.push(SelectItem::Expr {
24004                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
24005                    alias: Some(srch.set_column.clone()),
24006                });
24007                // rec depth = cte.set[1] + 1.
24008                let parent_depth = Expr::ArraySubscript {
24009                    target: Box::new(col_ref(&srch.set_column)),
24010                    index: Box::new(Expr::Literal(Literal::Integer(1))),
24011                };
24012                body.unions[rec].1.items.push(SelectItem::Expr {
24013                    expr: Expr::Array(alloc::vec![
24014                        Expr::Binary {
24015                            lhs: Box::new(parent_depth),
24016                            op: BinOp::Add,
24017                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
24018                        },
24019                        rec_key,
24020                    ]),
24021                    alias: Some(srch.set_column.clone()),
24022                });
24023            }
24024            extra_cols.push(srch.set_column);
24025        }
24026
24027        if let Some(cyc) = cycle {
24028            let positions: Vec<usize> = cyc
24029                .columns
24030                .iter()
24031                .map(|c| pos_of(c))
24032                .collect::<Result<_, _>>()?;
24033            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
24034            // cast it to text for the cycle path: membership only needs equality,
24035            // and the record text form gives SPG a TextArray path (SPG has no
24036            // typed record[] array). Cycle detection is unaffected.
24037            let base_row = Expr::Cast {
24038                expr: Box::new(row_of(&body.items, &positions)?),
24039                target: CastTarget::Text,
24040            };
24041            let rec_row = Expr::Cast {
24042                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
24043                target: CastTarget::Text,
24044            };
24045            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
24046            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
24047            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
24048            body.items.push(SelectItem::Expr {
24049                expr: Expr::Literal(dflt.clone()),
24050                alias: Some(cyc.mark_column.clone()),
24051            });
24052            body.items.push(SelectItem::Expr {
24053                expr: Expr::Array(alloc::vec![base_row]),
24054                alias: Some(cyc.path_column.clone()),
24055            });
24056            // rec mark: ROW(cols) already in the path → cycle.
24057            let hit = Expr::AnyAll {
24058                expr: Box::new(rec_row.clone()),
24059                op: BinOp::Eq,
24060                array: Box::new(col_ref(&cyc.path_column)),
24061                is_any: true,
24062            };
24063            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
24064                Expr::Case {
24065                    operand: None,
24066                    branches: alloc::vec![(hit, Expr::Literal(mark))],
24067                    else_branch: Some(Box::new(Expr::Literal(dflt))),
24068                }
24069            } else {
24070                hit
24071            };
24072            body.unions[rec].1.items.push(SelectItem::Expr {
24073                expr: mark_expr,
24074                alias: Some(cyc.mark_column.clone()),
24075            });
24076            // rec path: array_append(cte.path, ROW(cols)).
24077            body.unions[rec].1.items.push(SelectItem::Expr {
24078                expr: Expr::FunctionCall {
24079                    name: "array_append".into(),
24080                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
24081                },
24082                alias: Some(cyc.path_column.clone()),
24083            });
24084            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
24085            let stop = Expr::Unary {
24086                op: UnOp::Not,
24087                expr: Box::new(col_ref(&cyc.mark_column)),
24088            };
24089            let w = &mut body.unions[rec].1.where_;
24090            *w = Some(match w.take() {
24091                Some(prev) => Expr::Binary {
24092                    lhs: Box::new(prev),
24093                    op: BinOp::And,
24094                    rhs: Box::new(stop),
24095                },
24096                None => stop,
24097            });
24098            extra_cols.push(cyc.mark_column);
24099            extra_cols.push(cyc.path_column);
24100        }
24101        cte.column_overrides.extend(extra_cols);
24102        Ok(())
24103    }
24104
24105    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
24106    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
24107    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
24108        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
24109            return Ok(None);
24110        }
24111        self.advance(); // SEARCH
24112        let depth_first = match self.peek() {
24113            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
24114            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
24115            other => {
24116                return Err(self.err(format!(
24117                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
24118                )));
24119            }
24120        };
24121        self.advance();
24122        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
24123            return Err(self.err(format!(
24124                "expected FIRST after SEARCH mode, got {:?}",
24125                self.peek()
24126            )));
24127        }
24128        self.advance();
24129        if !self.peek_is_by() {
24130            return Err(self.err(format!(
24131                "expected BY after SEARCH … FIRST, got {:?}",
24132                self.peek()
24133            )));
24134        }
24135        self.advance();
24136        let mut by_columns = alloc::vec![self.expect_ident_like()?];
24137        while matches!(self.peek(), Token::Comma) {
24138            self.advance();
24139            by_columns.push(self.expect_ident_like()?);
24140        }
24141        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24142            return Err(self.err(format!(
24143                "expected SET in SEARCH clause, got {:?}",
24144                self.peek()
24145            )));
24146        }
24147        self.advance();
24148        let set_column = self.expect_ident_like()?;
24149        Ok(Some(crate::ast::SearchClause {
24150            depth_first,
24151            by_columns,
24152            set_column,
24153        }))
24154    }
24155
24156    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
24157    /// USING pathcol`. Returns None when the next token isn't CYCLE.
24158    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
24159        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
24160            return Ok(None);
24161        }
24162        self.advance(); // CYCLE
24163        let mut columns = alloc::vec![self.expect_ident_like()?];
24164        while matches!(self.peek(), Token::Comma) {
24165            self.advance();
24166            columns.push(self.expect_ident_like()?);
24167        }
24168        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24169            return Err(self.err(format!(
24170                "expected SET in CYCLE clause, got {:?}",
24171                self.peek()
24172            )));
24173        }
24174        self.advance();
24175        let mark_column = self.expect_ident_like()?;
24176        let mut mark_value = None;
24177        let mut default_value = None;
24178        if matches!(self.peek(), Token::To) {
24179            self.advance();
24180            mark_value = Some(self.parse_cycle_literal()?);
24181            if !matches!(self.peek(), Token::Default) {
24182                return Err(self.err(format!(
24183                    "expected DEFAULT after CYCLE … TO, got {:?}",
24184                    self.peek()
24185                )));
24186            }
24187            self.advance();
24188            default_value = Some(self.parse_cycle_literal()?);
24189        }
24190        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
24191            return Err(self.err(format!(
24192                "expected USING in CYCLE clause, got {:?}",
24193                self.peek()
24194            )));
24195        }
24196        self.advance();
24197        let path_column = self.expect_ident_like()?;
24198        Ok(Some(crate::ast::CycleClause {
24199            columns,
24200            mark_column,
24201            mark_value,
24202            default_value,
24203            path_column,
24204        }))
24205    }
24206
24207    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
24208    /// literal (string / bool / number) in PG.
24209    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
24210        match self.parse_expr(0)? {
24211            Expr::Literal(l) => Ok(l),
24212            other => Err(self.err(format!(
24213                "CYCLE mark/default value must be a literal, got {other:?}"
24214            ))),
24215        }
24216    }
24217
24218    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
24219        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
24220        // Comes through as an identifier; consume it if present and
24221        // mark every CTE in the clause as recursive (PG semantics —
24222        // the flag is per-WITH, not per-CTE).
24223        let mut recursive = false;
24224        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24225            && s.eq_ignore_ascii_case("recursive")
24226        {
24227            self.advance();
24228            recursive = true;
24229        }
24230        let mut ctes = Vec::new();
24231        loop {
24232            let name = self.expect_ident_like()?;
24233            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
24234            // PG uses these to rename the body's output columns; we
24235            // do the same below by overriding `columns[i].name`.
24236            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
24237                self.advance();
24238                let mut names = Vec::new();
24239                loop {
24240                    names.push(self.expect_ident_like()?);
24241                    if matches!(self.peek(), Token::Comma) {
24242                        self.advance();
24243                        continue;
24244                    }
24245                    break;
24246                }
24247                if !matches!(self.peek(), Token::RParen) {
24248                    return Err(self.err(format!(
24249                        "expected ')' to close CTE column list, got {:?}",
24250                        self.peek()
24251                    )));
24252                }
24253                self.advance();
24254                names
24255            } else {
24256                Vec::new()
24257            };
24258            // AS is a reserved Token::As (used by SELECT-item / FROM
24259            // aliasing) — handle it specially rather than as a bare
24260            // ident.
24261            if !matches!(self.peek(), Token::As) {
24262                return Err(self.err(format!(
24263                    "expected AS after CTE name {name:?}, got {:?}",
24264                    self.peek()
24265                )));
24266            }
24267            self.advance();
24268            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
24269            // MATERIALIZED` optimizer hints. SPG materialises every
24270            // CTE, so both spellings are accepted and absorbed.
24271            if matches!(self.peek(), Token::Not) {
24272                self.advance(); // NOT
24273                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24274                    if s.eq_ignore_ascii_case("materialized"))
24275                {
24276                    self.advance();
24277                } else {
24278                    return Err(self.err(format!(
24279                        "expected MATERIALIZED after AS NOT, got {:?}",
24280                        self.peek()
24281                    )));
24282                }
24283            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24284                if s.eq_ignore_ascii_case("materialized"))
24285            {
24286                self.advance();
24287            }
24288            if !matches!(self.peek(), Token::LParen) {
24289                return Err(self.err(format!(
24290                    "expected '(' after AS in WITH clause, got {:?}",
24291                    self.peek()
24292                )));
24293            }
24294            self.advance();
24295            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
24296            // RETURNING) as the CTE body in addition to SELECT.
24297            // PG writable CTE semantics. UPDATE / DELETE come in as
24298            // bare Idents (lexer keeps SELECT / INSERT as reserved
24299            // tokens but treats the rest of DML as case-insensitive
24300            // idents).
24301            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24302            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24303            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24304            let body = match self.peek() {
24305                Token::Select => {
24306                    let inner = self.parse_select_stmt()?;
24307                    let Statement::Select(s) = inner else {
24308                        unreachable!("parse_select_stmt returns Select");
24309                    };
24310                    crate::ast::CteBody::Select(s)
24311                }
24312                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24313                // `SELECT * FROM t` this way and accepts it wherever a
24314                // SELECT goes, so the CTE body dispatch needs its own
24315                // arm: this match is keyed on the FIRST token, and
24316                // `Token::Table` fell through to a tail that then
24317                // rejected what it got. `parse_table_shorthand` has
24318                // returned a desugared SelectStatement since the
24319                // shorthand landed — only the routing was missing.
24320                // Round 868 found this by putting the shorthand in a
24321                // subquery; every earlier check used a top-level form.
24322                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24323                // `SELECT * FROM t` this way and accepts it wherever a
24324                // SELECT goes, so the CTE body dispatch needs its own
24325                // arm: this match is keyed on the FIRST token, and
24326                // `Token::Table` fell through to a tail that rejected
24327                // what it got. `parse_table_shorthand` has returned a
24328                // desugared SelectStatement since the shorthand landed —
24329                // only the routing was missing, here and in the derived
24330                // table's second-token gate. Round 868 found both by
24331                // putting the shorthand in a subquery; every earlier
24332                // check had used a top-level form.
24333                Token::Table
24334                    if matches!(
24335                        self.tokens.get(self.pos + 1),
24336                        Some(Token::Ident(_) | Token::QuotedIdent(_))
24337                    ) =>
24338                {
24339                    let mut head = self.parse_table_shorthand()?;
24340                    self.parse_setop_chain_into(&mut head)?;
24341                    self.parse_select_tail_into(&mut head)?;
24342                    crate::ast::CteBody::Select(head)
24343                }
24344                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
24345                // WITH t(a) AS (VALUES (1), (2)) … lowers through
24346                // the shared rows helper onto a Select body.
24347                Token::Values => {
24348                    self.advance(); // VALUES
24349                    let mut head = self.parse_values_rows_body()?;
24350                    // A VALUES seed can head a set-operation chain —
24351                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
24352                    // SELECT n+1 FROM t …). Attach any trailing
24353                    // UNION / INTERSECT / EXCEPT peers so the
24354                    // recursive-CTE body parses like the SELECT seed.
24355                    self.parse_setop_chain_into(&mut head)?;
24356                    crate::ast::CteBody::Select(head)
24357                }
24358                Token::Insert => {
24359                    let inner = self.parse_one_statement()?;
24360                    let Statement::Insert(s) = inner else {
24361                        unreachable!("Token::Insert routes to Insert");
24362                    };
24363                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24364                }
24365                _ if is_update_kw => {
24366                    let inner = self.parse_one_statement()?;
24367                    let Statement::Update(s) = inner else {
24368                        return Err(
24369                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
24370                        );
24371                    };
24372                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24373                }
24374                _ if is_delete_kw => {
24375                    let inner = self.parse_one_statement()?;
24376                    let Statement::Delete(s) = inner else {
24377                        return Err(
24378                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
24379                        );
24380                    };
24381                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24382                }
24383                // v7.39 (round 149) — PG 17 allows MERGE as a
24384                // data-modifying CTE body.
24385                _ if is_merge_kw => {
24386                    let inner = self.parse_one_statement()?;
24387                    let Statement::Merge(s) = inner else {
24388                        return Err(
24389                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
24390                        );
24391                    };
24392                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24393                }
24394                // v7.39 (round 151) — a CTE body may itself be
24395                // WITH-headed (PG grammar: PreparableStmt carries its
24396                // own with_clause). The nested statement keeps its own
24397                // ctes; the modifying-CTE-at-top-level rule is enforced
24398                // at execution.
24399                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24400                    self.advance(); // WITH
24401                    match self.parse_with_cte_then_select()? {
24402                        Statement::Select(s) => crate::ast::CteBody::Select(s),
24403                        Statement::Insert(s) => {
24404                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24405                        }
24406                        Statement::Update(s) => {
24407                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24408                        }
24409                        Statement::Delete(s) => {
24410                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24411                        }
24412                        Statement::Merge(s) => {
24413                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24414                        }
24415
24416                        other => {
24417                            return Err(self.err(format!(
24418                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24419                            )));
24420                        }
24421                    }
24422                }
24423                other => {
24424                    return Err(self.err(format!(
24425                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24426                    )));
24427                }
24428            };
24429            if !matches!(self.peek(), Token::RParen) {
24430                return Err(self.err(format!(
24431                    "expected ')' after CTE body, got {:?}",
24432                    self.peek()
24433                )));
24434            }
24435            self.advance();
24436            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
24437            // CTE, desugared into extra body columns by the engine.
24438            let search = self.parse_cte_search_clause()?;
24439            let cycle = self.parse_cte_cycle_clause()?;
24440            let mut cte = crate::ast::Cte {
24441                name,
24442                body,
24443                recursive,
24444                column_overrides,
24445                search,
24446                cycle,
24447            };
24448            self.validate_recursive_cte(&cte)?;
24449            self.desugar_cte_search_cycle(&mut cte)?;
24450            ctes.push(cte);
24451            if matches!(self.peek(), Token::Comma) {
24452                self.advance();
24453                continue;
24454            }
24455            break;
24456        }
24457        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
24458        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
24459        // the parsed CTEs to whichever statement the body produces.
24460        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24461        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24462        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24463        match self.peek() {
24464            Token::Select => {
24465                let body_stmt = self.parse_select_stmt()?;
24466                let Statement::Select(mut body) = body_stmt else {
24467                    unreachable!()
24468                };
24469                body.ctes = ctes;
24470                Ok(Statement::Select(body))
24471            }
24472            Token::Insert => {
24473                let body_stmt = self.parse_one_statement()?;
24474                let Statement::Insert(mut body) = body_stmt else {
24475                    unreachable!()
24476                };
24477                body.ctes = ctes;
24478                Ok(Statement::Insert(body))
24479            }
24480            _ if outer_is_update => {
24481                let body_stmt = self.parse_one_statement()?;
24482                let Statement::Update(mut body) = body_stmt else {
24483                    return Err(self.err(format!("expected UPDATE after WITH clause")));
24484                };
24485                body.ctes = ctes;
24486                Ok(Statement::Update(body))
24487            }
24488            _ if outer_is_delete => {
24489                let body_stmt = self.parse_one_statement()?;
24490                let Statement::Delete(mut body) = body_stmt else {
24491                    return Err(self.err(format!("expected DELETE after WITH clause")));
24492                };
24493                body.ctes = ctes;
24494                Ok(Statement::Delete(body))
24495            }
24496            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
24497            // WITH RECURSIVE is rejected with PG's exact message
24498            // (parse analysis, transformWithClause).
24499            _ if outer_is_merge => {
24500                if recursive {
24501                    return Err(self.err(String::from(
24502                        "WITH RECURSIVE is not supported for MERGE statement",
24503                    )));
24504                }
24505                let body_stmt = self.parse_one_statement()?;
24506                let Statement::Merge(mut body) = body_stmt else {
24507                    return Err(self.err(format!("expected MERGE after WITH clause")));
24508                };
24509                body.ctes = ctes;
24510                Ok(Statement::Merge(body))
24511            }
24512            other => Err(self.err(format!(
24513                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
24514            ))),
24515        }
24516    }
24517
24518    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
24519    /// already consumed the leading `EXISTS` ident via
24520    /// `self.advance()`.
24521    /// v7.13.0 — parse the rest of a `CASE … END` expression after
24522    /// the leading `CASE` ident has been consumed (mailrs round-5
24523    /// G9). Supports both the searched form
24524    /// (`CASE WHEN cond THEN val …`) and the simple form
24525    /// (`CASE operand WHEN val THEN val …`).
24526    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
24527        // Disambiguate searched vs simple form: if the next token
24528        // is `WHEN`, we're in the searched form. Otherwise the
24529        // intervening expression is the operand.
24530        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
24531            None
24532        } else {
24533            Some(Box::new(self.parse_expr(0)?))
24534        };
24535        let mut branches: Vec<(Expr, Expr)> = Vec::new();
24536        loop {
24537            match self.peek() {
24538                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
24539                    self.advance();
24540                    let cond = self.parse_expr(0)?;
24541                    match self.peek() {
24542                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
24543                            self.advance();
24544                        }
24545                        other => {
24546                            return Err(self.err(alloc::format!(
24547                                "expected THEN after CASE WHEN <expr>, got {other:?}"
24548                            )));
24549                        }
24550                    }
24551                    let value = self.parse_expr(0)?;
24552                    branches.push((cond, value));
24553                }
24554                _ => break,
24555            }
24556        }
24557        if branches.is_empty() {
24558            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
24559        }
24560        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
24561        {
24562            self.advance();
24563            Some(Box::new(self.parse_expr(0)?))
24564        } else {
24565            None
24566        };
24567        match self.peek() {
24568            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
24569                self.advance();
24570            }
24571            other => {
24572                return Err(self.err(alloc::format!(
24573                    "expected END to close CASE expression, got {other:?}"
24574                )));
24575            }
24576        }
24577        Ok(Expr::Case {
24578            operand,
24579            branches,
24580            else_branch,
24581        })
24582    }
24583
24584    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
24585    /// query-source position (EXISTS / IN / INSERT source / CTE body /
24586    /// view body). Caller consumed the WITH keyword. Only a SELECT
24587    /// outer is grammatical here; the data-modifying-CTE-at-top-level
24588    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
24589    /// maps correctly.
24590    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24591        let inner = self.parse_with_cte_then_select()?;
24592        match inner {
24593            Statement::Select(s) => Ok(s),
24594            other => Err(self.err(format!(
24595                "expected SELECT after WITH in a subquery, got {other:?}"
24596            ))),
24597        }
24598    }
24599
24600    /// True when the next token is the (unquoted) WITH keyword. WITH is
24601    /// reserved in PG, so a bare `with` can never be a column reference
24602    /// in these positions; a quoted `"with"` stays an identifier.
24603    fn peek_is_with_kw(&self) -> bool {
24604        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
24605    }
24606
24607    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
24608    /// `#[inline(never)]` keeps the large SelectStatement temporaries
24609    /// off parse_expr's recursive frame (the nesting-budget stack
24610    /// cliff — see the round-153 gate regression).
24611    #[inline(never)]
24612    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24613        if self.peek_is_with_kw() {
24614            self.advance();
24615            self.parse_nested_with_select()
24616        } else {
24617            match self.parse_select_stmt()? {
24618                Statement::Select(s) => Ok(s),
24619                other => Err(self.err(alloc::format!(
24620                    "expected SELECT inside ANY/ALL, got {other:?}"
24621                ))),
24622            }
24623        }
24624    }
24625
24626    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
24627        if !matches!(self.peek(), Token::LParen) {
24628            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
24629        }
24630        self.advance();
24631        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
24632        let s = if self.peek_is_with_kw() {
24633            self.advance();
24634            self.parse_nested_with_select()?
24635        } else {
24636            let inner = self.parse_select_stmt()?;
24637            let Statement::Select(s) = inner else {
24638                unreachable!("parse_select_stmt returns Select")
24639            };
24640            s
24641        };
24642        if !matches!(self.peek(), Token::RParen) {
24643            return Err(self.err(format!(
24644                "expected ')' after EXISTS-subquery, got {:?}",
24645                self.peek()
24646            )));
24647        }
24648        self.advance();
24649        Ok(Expr::Exists {
24650            subquery: Box::new(s),
24651            negated,
24652        })
24653    }
24654
24655    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24656        self.advance(); // IN
24657        if !matches!(self.peek(), Token::LParen) {
24658            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
24659        }
24660        self.advance();
24661        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
24662        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
24663        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
24664            let s = if self.peek_is_with_kw() {
24665                self.advance();
24666                self.parse_nested_with_select()?
24667            } else {
24668                let inner = self.parse_select_stmt()?;
24669                let Statement::Select(s) = inner else {
24670                    unreachable!("parse_select_stmt always returns Statement::Select")
24671                };
24672                s
24673            };
24674            if !matches!(self.peek(), Token::RParen) {
24675                return Err(self.err(format!(
24676                    "expected ')' after IN-subquery, got {:?}",
24677                    self.peek()
24678                )));
24679            }
24680            self.advance();
24681            return Ok(Expr::InSubquery {
24682                expr: Box::new(expr),
24683                subquery: Box::new(s),
24684                negated,
24685            });
24686        }
24687        let mut elements = Vec::new();
24688        if !matches!(self.peek(), Token::RParen) {
24689            loop {
24690                elements.push(self.parse_expr(0)?);
24691                match self.peek() {
24692                    Token::Comma => {
24693                        self.advance();
24694                    }
24695                    Token::RParen => break,
24696                    other => {
24697                        return Err(
24698                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24699                        );
24700                    }
24701                }
24702            }
24703        }
24704        self.advance(); // ')'
24705        // v7.30.2 (mailrs round-25) — flat InList node instead of a
24706        // left-deep OR-Eq chain: chain depth scaled with the element
24707        // count and overflowed the stack (eval + drop are recursive).
24708        if elements.is_empty() {
24709            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24710        }
24711        Ok(Expr::InList {
24712            expr: Box::new(expr),
24713            list: elements,
24714            negated,
24715        })
24716    }
24717
24718    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24719    /// already consumed by the caller. Elements must be numeric literals
24720    /// (with optional unary `-`); any compound expression is rejected at
24721    /// parse time so the runtime never needs to evaluate inside a vector.
24722    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24723    /// has already consumed the `EXTRACT` token before calling us —
24724    /// we pick up at the opening `(`.
24725    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24726    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24727    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24728    /// per-column OR-fold of
24729    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24730    /// term)` so the existing FTS evaluator handles semantics.
24731    ///
24732    /// The mode modifier is accepted-and-ignored at v7.17 — all
24733    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24734    /// mode operators (`+foo -bar`) would need their own parser
24735    /// (Phase 2.2c); customers who hit them today already get a
24736    /// correct lexeme-match against the bare term, only without
24737    /// the +/- precedence the customer asked for.
24738    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24739        // Already at `MATCH`-consumed position; the dispatcher
24740        // confirmed the next token is `(`.
24741        if !matches!(self.peek(), Token::LParen) {
24742            return Err(self.err(alloc::format!(
24743                "expected '(' after MATCH, got {:?}",
24744                self.peek()
24745            )));
24746        }
24747        self.advance();
24748        let mut cols: Vec<Expr> = Vec::new();
24749        loop {
24750            cols.push(self.parse_expr(0)?);
24751            match self.peek() {
24752                Token::Comma => {
24753                    self.advance();
24754                }
24755                Token::RParen => break,
24756                other => {
24757                    return Err(self.err(alloc::format!(
24758                        "expected ',' or ')' in MATCH column list, got {other:?}"
24759                    )));
24760                }
24761            }
24762        }
24763        self.advance(); // ')'
24764        // Expect AGAINST.
24765        match self.peek() {
24766            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24767                self.advance();
24768            }
24769            other => {
24770                return Err(self.err(alloc::format!(
24771                    "expected AGAINST after MATCH column list, got {other:?}"
24772                )));
24773            }
24774        }
24775        if !matches!(self.peek(), Token::LParen) {
24776            return Err(self.err(alloc::format!(
24777                "expected '(' after AGAINST, got {:?}",
24778                self.peek()
24779            )));
24780        }
24781        self.advance();
24782        // Read AGAINST's argument as a single primary token —
24783        // string literal, placeholder, or column-ref ident. We
24784        // can't call `parse_expr` / `parse_unary` here because
24785        // the postfix chain inside `parse_atom` would greedily
24786        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24787        // and fail at "expected '(' after IN". Customers always
24788        // write a literal or bound parameter in AGAINST, so this
24789        // restriction is non-blocking; the error path explains
24790        // the limit if a more complex expression shows up.
24791        let term = match self.advance() {
24792            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24793            Token::Placeholder(n) => Expr::Placeholder(n),
24794            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24795                qualifier: None,
24796                name: s,
24797            }),
24798            other => {
24799                return Err(self.err(alloc::format!(
24800                    "MATCH ... AGAINST(<term>) expects a string literal, \
24801                     bound parameter, or column ref, got {other:?}"
24802                )));
24803            }
24804        };
24805        // Optional mode tail — accept-and-ignore at v7.17:
24806        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24807        //   IN BOOLEAN MODE
24808        //   WITH QUERY EXPANSION
24809        loop {
24810            match self.peek() {
24811                // IN lexes as a reserved Token::In, not an ident,
24812                // so it gets its own arm.
24813                Token::In => {
24814                    self.advance();
24815                }
24816                Token::Ident(s) | Token::QuotedIdent(s)
24817                    if s.eq_ignore_ascii_case("natural")
24818                        || s.eq_ignore_ascii_case("language")
24819                        || s.eq_ignore_ascii_case("boolean")
24820                        || s.eq_ignore_ascii_case("mode")
24821                        || s.eq_ignore_ascii_case("with")
24822                        || s.eq_ignore_ascii_case("query")
24823                        || s.eq_ignore_ascii_case("expansion") =>
24824                {
24825                    self.advance();
24826                }
24827                _ => break,
24828            }
24829        }
24830        if !matches!(self.peek(), Token::RParen) {
24831            return Err(self.err(alloc::format!(
24832                "expected ')' to close AGAINST, got {:?}",
24833                self.peek()
24834            )));
24835        }
24836        self.advance();
24837        // Build per-column `to_tsvector('simple', col) @@
24838        // plainto_tsquery('simple', term)` and OR-fold.
24839        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24840        let plainto = Expr::FunctionCall {
24841            name: String::from("plainto_tsquery"),
24842            args: alloc::vec![simple_lit(), term.clone()],
24843        };
24844        let mut folded: Option<Expr> = None;
24845        for col in cols {
24846            let to_tsv = Expr::FunctionCall {
24847                name: String::from("to_tsvector"),
24848                args: alloc::vec![simple_lit(), col],
24849            };
24850            let leaf = Expr::Binary {
24851                lhs: Box::new(to_tsv),
24852                op: crate::ast::BinOp::TsMatch,
24853                rhs: Box::new(plainto.clone()),
24854            };
24855            folded = Some(match folded {
24856                None => leaf,
24857                Some(prev) => Expr::Binary {
24858                    lhs: Box::new(prev),
24859                    op: crate::ast::BinOp::Or,
24860                    rhs: Box::new(leaf),
24861                },
24862            });
24863        }
24864        match folded {
24865            Some(e) => Ok(e),
24866            None => Err(self.err(String::from(
24867                "MATCH(...) AGAINST(...) requires at least one column",
24868            ))),
24869        }
24870    }
24871
24872    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24873        if !matches!(self.peek(), Token::LParen) {
24874            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24875        }
24876        self.advance();
24877        let field_name = self.expect_ident_like()?;
24878        let field = match field_name.to_ascii_lowercase().as_str() {
24879            // PG accepts the plural spellings (years/months/…/millenniums) as
24880            // aliases for the singular fields — its datetime unit table has both.
24881            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24882            "year" | "years" => ExtractField::Year,
24883            "month" | "months" => ExtractField::Month,
24884            "day" | "days" => ExtractField::Day,
24885            "hour" | "hours" => ExtractField::Hour,
24886            "minute" | "minutes" => ExtractField::Minute,
24887            "second" | "seconds" => ExtractField::Second,
24888            "microsecond" | "microseconds" => ExtractField::Microsecond,
24889            "epoch" => ExtractField::Epoch,
24890            "dow" => ExtractField::Dow,
24891            "isodow" => ExtractField::Isodow,
24892            "doy" => ExtractField::Doy,
24893            "week" | "weeks" => ExtractField::Week,
24894            "isoyear" => ExtractField::Isoyear,
24895            "quarter" => ExtractField::Quarter,
24896            "decade" | "decades" => ExtractField::Decade,
24897            "century" | "centuries" => ExtractField::Century,
24898            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24899            "julian" => ExtractField::Julian,
24900            "millisecond" | "milliseconds" => ExtractField::Millisecond,
24901            "timezone" => ExtractField::Timezone,
24902            "timezone_hour" => ExtractField::TimezoneHour,
24903            "timezone_minute" => ExtractField::TimezoneMinute,
24904            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24905            // reports an unknown one with the source type (22023); carry the
24906            // raw name so eval can word it.
24907            other => ExtractField::Other(alloc::string::String::from(other)),
24908        };
24909        if !matches!(self.peek(), Token::From) {
24910            return Err(self.err(format!(
24911                "expected FROM after EXTRACT field, got {:?}",
24912                self.peek()
24913            )));
24914        }
24915        self.advance();
24916        let source = self.parse_expr(0)?;
24917        if !matches!(self.peek(), Token::RParen) {
24918            return Err(self.err(format!(
24919                "expected ')' to close EXTRACT, got {:?}",
24920                self.peek()
24921            )));
24922        }
24923        self.advance();
24924        Ok(Expr::Extract {
24925            field,
24926            source: Box::new(source),
24927        })
24928    }
24929
24930    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24931    /// is already consumed; we expect a single string literal next and
24932    /// resolve it into `Literal::Interval` at parse time so the engine
24933    /// never has to re-tokenise inside the string.
24934    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24935    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24936    /// is the SQL-standard form and is left to the path below.
24937    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24938        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24939        let (offset, sign) = match self.peek() {
24940            Token::Minus => (1, "-"),
24941            _ => (0, ""),
24942        };
24943        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24944            return None;
24945        };
24946        self.tokens
24947            .get(self.pos + offset + 1)
24948            .filter(|t| mysql_interval_unit(t).is_some())?;
24949        Some((alloc::format!("{sign}{n}"), offset + 1))
24950    }
24951
24952    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24953    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24954    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24955    ///
24956    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24957    /// this by parsing the group and then restoring `self.pos` — which could
24958    /// never have worked, because `advance()` DESTROYS the token it returns
24959    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24960    /// inert only because both branches errored back then.
24961    fn interval_paren_is_quantity(&self) -> bool {
24962        let mut depth = 0usize;
24963        let mut saw_top_level_comma = false;
24964        let mut i = self.pos;
24965        while let Some(tok) = self.tokens.get(i) {
24966            match tok {
24967                Token::LParen => depth += 1,
24968                Token::RParen => {
24969                    depth = depth.saturating_sub(1);
24970                    if depth == 0 {
24971                        return !saw_top_level_comma
24972                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24973                                .is_some();
24974                    }
24975                }
24976                // A comma directly inside the outermost parens means the
24977                // argument list of the INTERVAL() function.
24978                Token::Comma if depth == 1 => saw_top_level_comma = true,
24979                Token::Eof => return false,
24980                _ => {}
24981            }
24982            i += 1;
24983        }
24984        false
24985    }
24986
24987    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24988        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24989        // (the index of the last Ni ≤ N), distinct from the interval literal.
24990        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24991        // is decided by a non-destructive lookahead (round 422) before either
24992        // branch consumes anything. MySQL only.
24993        if self.mysql_dialect
24994            && matches!(self.peek(), Token::LParen)
24995            && !self.interval_paren_is_quantity()
24996        {
24997            self.advance(); // (
24998            let mut args = Vec::new();
24999            if !matches!(self.peek(), Token::RParen) {
25000                loop {
25001                    args.push(self.parse_expr(0)?);
25002                    if matches!(self.peek(), Token::Comma) {
25003                        self.advance();
25004                        continue;
25005                    }
25006                    break;
25007                }
25008            }
25009            if !matches!(self.peek(), Token::RParen) {
25010                return Err(self.err(alloc::format!(
25011                    "expected ')' after INTERVAL() arguments, got {:?}",
25012                    self.peek()
25013                )));
25014            }
25015            self.advance(); // )
25016            return Ok(Expr::FunctionCall {
25017                name: alloc::string::String::from("interval"),
25018                args,
25019            });
25020        }
25021        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
25022        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
25023        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
25024        // writes every date arithmetic there is, and it did not parse at
25025        // all. PG rejects the unquoted form outright (`syntax error at or
25026        // near "1"`, measured), so it is taken only in the MySQL dialect —
25027        // PG's own `INTERVAL '1' DAY` is untouched below.
25028        if self.mysql_dialect
25029            && let Some((text, consume)) = self.peek_unquoted_interval_count()
25030        {
25031            for _ in 0..consume {
25032                self.advance(); // the optional `-` and the number
25033            }
25034            let Some(unit) = mysql_interval_unit(self.peek()) else {
25035                return Err(self.err(alloc::format!(
25036                    "expected an interval unit after INTERVAL {text}, got {:?}",
25037                    self.peek()
25038                )));
25039            };
25040            self.advance(); // the unit
25041            let (months, days, micros) = scale_mysql_interval(&text, unit)
25042                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
25043            return Ok(Expr::Literal(Literal::Interval {
25044                months,
25045                days,
25046                micros,
25047                // The canonical rendering, so Display round-trips into a
25048                // form both dialects read back.
25049                text: alloc::format!("{text} {unit}"),
25050            }));
25051        }
25052        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
25053        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
25054        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
25055        // Those cannot fold into a compile-time `Literal::Interval`, so they
25056        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
25057        // builtin, which builds the value at run time (and yields NULL for a
25058        // NULL quantity, as MariaDB does). The literal path above still folds
25059        // the constant case — it is cheaper and round-trips through Display.
25060        //
25061        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
25062        // MySQL's quoted spelling) keep the qualifier path below.
25063        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
25064            let qty = self.parse_expr(0)?;
25065            let Some(unit) = mysql_interval_unit(self.peek()) else {
25066                return Err(self.err(alloc::format!(
25067                    "expected an interval unit after INTERVAL <expr>, got {:?}",
25068                    self.peek()
25069                )));
25070            };
25071            self.advance(); // the unit
25072            return Ok(make_interval_call(qty, unit));
25073        }
25074        let tok = self.advance();
25075        let Token::String(text) = tok else {
25076            return Err(self.err(format!(
25077                "expected string literal after INTERVAL, got {tok:?}"
25078            )));
25079        };
25080        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
25081        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
25082        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
25083        // bare number means and the leading/trailing precision.
25084        let field1 = interval_field_of(self.peek());
25085        let qualifier = if let Some(f1) = field1 {
25086            self.advance();
25087            let f2 = if matches!(self.peek(), Token::To) {
25088                self.advance();
25089                let Some(f) = interval_field_of(self.peek()) else {
25090                    return Err(self.err(format!(
25091                        "expected an interval field after TO, got {:?}",
25092                        self.peek()
25093                    )));
25094                };
25095                self.advance();
25096                Some(f)
25097            } else {
25098                None
25099            };
25100            Some((f1, f2))
25101        } else {
25102            None
25103        };
25104        let (months, days, micros) = match qualifier {
25105            Some(q) => interpret_qualified_interval(&text, q),
25106            None => parse_interval_text(&text),
25107        }
25108        .ok_or_else(|| ParseError {
25109            message: format!(
25110                "cannot parse INTERVAL {text:?}; \
25111                     expected `<n> <unit> [<n> <unit> ...]` with units \
25112                     microsecond[s], millisecond[s], second[s], minute[s], \
25113                     hour[s], day[s], week[s], month[s], year[s]"
25114            ),
25115            token_pos: self.consumed_pos(),
25116        })?;
25117        Ok(Expr::Literal(Literal::Interval {
25118            months,
25119            days,
25120            micros,
25121            text,
25122        }))
25123    }
25124
25125    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
25126    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
25127    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
25128    /// than a pgvector literal.
25129    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
25130        self.advance(); // consume `[`
25131        let mut items: Vec<Expr> = Vec::new();
25132        if !matches!(self.peek(), Token::RBracket) {
25133            loop {
25134                if matches!(self.peek(), Token::LBracket) {
25135                    items.push(self.parse_array_bracket_body()?);
25136                } else {
25137                    items.push(self.parse_expr(0)?);
25138                }
25139                match self.peek() {
25140                    Token::Comma => {
25141                        self.advance();
25142                    }
25143                    Token::RBracket => break,
25144                    other => {
25145                        return Err(self.err(alloc::format!(
25146                            "expected ',' or ']' in array literal, got {other:?}"
25147                        )));
25148                    }
25149                }
25150            }
25151        }
25152        self.advance(); // consume `]`
25153        Ok(Expr::Array(items))
25154    }
25155
25156    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
25157        let mut elems = Vec::new();
25158        if matches!(self.peek(), Token::RBracket) {
25159            self.advance();
25160            return Ok(Expr::Literal(Literal::Vector(elems)));
25161        }
25162        loop {
25163            let e = self.parse_expr(0)?;
25164            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
25165                message: format!("vector element must be a numeric literal, got {e:?}"),
25166                token_pos: self.pos,
25167            })?;
25168            elems.push(x);
25169            match self.peek() {
25170                Token::Comma => {
25171                    self.advance();
25172                }
25173                Token::RBracket => {
25174                    self.advance();
25175                    break;
25176                }
25177                other => {
25178                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
25179                }
25180            }
25181        }
25182        Ok(Expr::Literal(Literal::Vector(elems)))
25183    }
25184
25185    /// Atom that started with an identifier: could be `t.col`, `col`, or
25186    /// `func(arg, ...)`. Detect each shape by looking at the next token.
25187    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
25188    /// [, ...])`. Caller has already consumed `OVER`. Either clause
25189    /// is optional; an empty `()` is also legal (PG semantics).
25190    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
25191    /// modifier between `name(args)` and `OVER (...)`. Default is
25192    /// `Respect`. Unrecognised idents leave the stream unchanged.
25193    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
25194        let Token::Ident(s) = self.peek().clone() else {
25195            return NullTreatment::Respect;
25196        };
25197        let is_ignore = s.eq_ignore_ascii_case("ignore");
25198        let is_respect = s.eq_ignore_ascii_case("respect");
25199        if !is_ignore && !is_respect {
25200            return NullTreatment::Respect;
25201        }
25202        // Lookahead for NULLS — only consume both tokens together.
25203        // pos+1 must hold a "nulls" ident.
25204        if self.pos + 1 < self.tokens.len()
25205            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
25206            && s2.eq_ignore_ascii_case("nulls")
25207        {
25208            self.advance();
25209            self.advance();
25210            return if is_ignore {
25211                NullTreatment::Ignore
25212            } else {
25213                NullTreatment::Respect
25214            };
25215        }
25216        NullTreatment::Respect
25217    }
25218
25219    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
25220    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
25221    /// (same shape as the `OVER` tail). Consumes the whole clause and
25222    /// returns the predicate; returns `None` when no `FILTER` follows.
25223    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
25224        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25225            return Ok(None);
25226        };
25227        if !s.eq_ignore_ascii_case("filter") {
25228            return Ok(None);
25229        }
25230        self.advance(); // FILTER
25231        if !matches!(self.peek(), Token::LParen) {
25232            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
25233        }
25234        self.advance(); // (
25235        if !matches!(self.peek(), Token::Where) {
25236            return Err(self.err(format!(
25237                "expected WHERE inside FILTER (...), got {:?}",
25238                self.peek()
25239            )));
25240        }
25241        self.advance(); // WHERE
25242        let cond = self.parse_expr(0)?;
25243        if !matches!(self.peek(), Token::RParen) {
25244            return Err(self.err(format!(
25245                "expected ')' to close FILTER (WHERE ...), got {:?}",
25246                self.peek()
25247            )));
25248        }
25249        self.advance(); // )
25250        Ok(Some(Box::new(cond)))
25251    }
25252
25253    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
25254    /// the separator as the aggregate's second argument, which is the
25255    /// shape `string_agg` already takes. Returns whether one was there.
25256    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
25257        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
25258            return Ok(false);
25259        }
25260        self.advance();
25261        let Token::String(sep) = self.peek().clone() else {
25262            return Err(self.err(alloc::format!(
25263                "expected a string literal after SEPARATOR, got {:?}",
25264                self.peek()
25265            )));
25266        };
25267        self.advance();
25268        args.push(Expr::Literal(Literal::String(sep)));
25269        Ok(true)
25270    }
25271
25272    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
25273    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
25274    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
25275    /// keys, or an empty vec when no `WITHIN GROUP` follows.
25276    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
25277        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25278            return Ok(Vec::new());
25279        };
25280        if !s.eq_ignore_ascii_case("within") {
25281            return Ok(Vec::new());
25282        }
25283        self.advance(); // WITHIN
25284        if !matches!(self.peek(), Token::Group) {
25285            return Err(self.err(format!(
25286                "expected GROUP after WITHIN, got {:?}",
25287                self.peek()
25288            )));
25289        }
25290        self.advance(); // GROUP
25291        if !matches!(self.peek(), Token::LParen) {
25292            return Err(self.err(format!(
25293                "expected '(' after WITHIN GROUP, got {:?}",
25294                self.peek()
25295            )));
25296        }
25297        self.advance(); // (
25298        if !matches!(self.peek(), Token::Order) {
25299            return Err(self.err(format!(
25300                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
25301                self.peek()
25302            )));
25303        }
25304        self.advance(); // ORDER
25305        if !self.peek_is_by() {
25306            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25307        }
25308        self.advance(); // BY
25309        let mut keys: Vec<OrderBy> = Vec::new();
25310        loop {
25311            // v7.39 (round 691) — save/restore, the discipline this parser
25312            // already uses around `pending_sample_preds`, so a subquery inside
25313            // a key neither inherits nor leaks the channel.
25314            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25315            let saved_coll = self.order_key_collation.take();
25316            let parsed = self.parse_expr(0);
25317            self.in_order_by_key = saved_flag;
25318            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
25319            let expr = parsed?;
25320            let desc = if matches!(self.peek(), Token::Desc) {
25321                self.advance();
25322                true
25323            } else if matches!(self.peek(), Token::Asc) {
25324                self.advance();
25325                false
25326            } else {
25327                false
25328            };
25329            let nulls_first = self.parse_optional_nulls_placement()?;
25330            keys.push(OrderBy {
25331                expr,
25332                desc,
25333                nulls_first,
25334                collation,
25335            });
25336            if matches!(self.peek(), Token::Comma) {
25337                self.advance();
25338            } else {
25339                break;
25340            }
25341        }
25342        if !matches!(self.peek(), Token::RParen) {
25343            return Err(self.err(format!(
25344                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
25345                self.peek()
25346            )));
25347        }
25348        self.advance(); // )
25349        Ok(keys)
25350    }
25351
25352    /// No frame clause is supported.
25353    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
25354    fn parse_over_clause(
25355        &mut self,
25356    ) -> Result<
25357        (
25358            Vec<Expr>,
25359            Vec<(Expr, bool, Option<bool>)>,
25360            Option<WindowFrame>,
25361        ),
25362        ParseError,
25363    > {
25364        // `OVER w` — a named-window reference. The WINDOW clause
25365        // parses after the select list, so the name rides out as a
25366        // marker in partition_by; parse_bare_select substitutes the
25367        // definition once the clause is known.
25368        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
25369            let name = w.clone();
25370            self.advance();
25371            return Ok((
25372                alloc::vec![Expr::Column(crate::ast::ColumnName {
25373                    qualifier: Some("__named_window__".to_string()),
25374                    name,
25375                })],
25376                Vec::new(),
25377                None,
25378            ));
25379        }
25380        if !matches!(self.peek(), Token::LParen) {
25381            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
25382        }
25383        self.advance();
25384        let mut partition_by = Vec::new();
25385        let mut order_by = Vec::new();
25386        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
25387        // window, refined in place. PG's rules (probed against 18.4) differ
25388        // from the bare `OVER w1` form, so the reference rides out under its
25389        // own marker and `substitute_named_windows` applies them. The base
25390        // name is any leading identifier that isn't a window-spec keyword.
25391        let base_window = match self.peek() {
25392            Token::Ident(s) | Token::QuotedIdent(s)
25393                if !s.eq_ignore_ascii_case("partition")
25394                    && !s.eq_ignore_ascii_case("rows")
25395                    && !s.eq_ignore_ascii_case("range")
25396                    && !s.eq_ignore_ascii_case("groups") =>
25397            {
25398                let n = s.clone();
25399                self.advance();
25400                Some(n)
25401            }
25402            _ => None,
25403        };
25404        // PARTITION BY ?
25405        // v7.37.6-B promoted PARTITION to a reserved keyword
25406        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25407        // `Token::Ident("partition")`. Accept both so older sources
25408        // and the new lexer surface land on the same path.
25409        let is_partition_kw = match self.peek() {
25410            Token::Partition => true,
25411            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25412            _ => false,
25413        };
25414        if is_partition_kw {
25415            self.advance();
25416            if !self.peek_is_by() {
25417                return Err(self.err(format!(
25418                    "expected BY after PARTITION, got {:?}",
25419                    self.peek()
25420                )));
25421            }
25422            self.advance();
25423            loop {
25424                partition_by.push(self.parse_expr(0)?);
25425                if matches!(self.peek(), Token::Comma) {
25426                    self.advance();
25427                    continue;
25428                }
25429                break;
25430            }
25431        }
25432        // ORDER BY ?
25433        if matches!(self.peek(), Token::Order) {
25434            self.advance();
25435            if !self.peek_is_by() {
25436                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25437            }
25438            self.advance();
25439            loop {
25440                let e = self.parse_expr(0)?;
25441                let desc = if matches!(self.peek(), Token::Desc) {
25442                    self.advance();
25443                    true
25444                } else if matches!(self.peek(), Token::Asc) {
25445                    self.advance();
25446                    false
25447                } else {
25448                    false
25449                };
25450                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
25451                let nulls_first = self.parse_optional_nulls_placement()?;
25452                order_by.push((e, desc, nulls_first));
25453                if matches!(self.peek(), Token::Comma) {
25454                    self.advance();
25455                    continue;
25456                }
25457                break;
25458            }
25459        }
25460        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
25461        // Both keywords come through the lexer as identifiers; match
25462        // case-insensitively.
25463        let mut frame: Option<WindowFrame> = None;
25464        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
25465            let kind = if s.eq_ignore_ascii_case("rows") {
25466                Some(FrameKind::Rows)
25467            } else if s.eq_ignore_ascii_case("range") {
25468                Some(FrameKind::Range)
25469            } else if s.eq_ignore_ascii_case("groups") {
25470                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
25471                Some(FrameKind::Groups)
25472            } else {
25473                None
25474            };
25475            if let Some(kind) = kind {
25476                self.advance();
25477                frame = Some(self.parse_frame_tail(kind)?);
25478            }
25479        }
25480        if !matches!(self.peek(), Token::RParen) {
25481            return Err(self.err(format!(
25482                "expected ')' to close OVER clause, got {:?}",
25483                self.peek()
25484            )));
25485        }
25486        self.advance();
25487        if let Some(base) = base_window {
25488            // A copy may refine but never override the base's partitioning
25489            // (PG rejects it outright, before looking the name up).
25490            if !partition_by.is_empty() {
25491                return Err(self.err(alloc::format!(
25492                    "cannot override PARTITION BY clause of window \"{base}\""
25493                )));
25494            }
25495            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
25496                qualifier: Some("__named_window_ref__".to_string()),
25497                name: base,
25498            })];
25499        }
25500        Ok((partition_by, order_by, frame))
25501    }
25502
25503    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
25504    /// or `RANGE` keyword was just consumed. Accepts both
25505    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
25506    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
25507    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
25508    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
25509        let (start, end) = if matches!(self.peek(), Token::Between) {
25510            self.advance();
25511            let start = self.parse_frame_bound()?;
25512            if !matches!(self.peek(), Token::And) {
25513                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
25514            }
25515            self.advance();
25516            let end = self.parse_frame_bound()?;
25517            (start, Some(end))
25518        } else {
25519            (self.parse_frame_bound()?, None)
25520        };
25521        let exclude = self.parse_frame_exclusion()?;
25522        Ok(WindowFrame {
25523            kind,
25524            start,
25525            end,
25526            exclude,
25527        })
25528    }
25529
25530    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
25531    /// after a frame spec. NO OTHERS is the default no-op.
25532    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
25533        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
25534            return Ok(FrameExclusion::NoOthers);
25535        }
25536        self.advance(); // EXCLUDE
25537        match self.peek() {
25538            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
25539                self.advance();
25540                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
25541                    return Err(self.err(format!(
25542                        "expected ROW after EXCLUDE CURRENT, got {:?}",
25543                        self.peek()
25544                    )));
25545                }
25546                self.advance();
25547                Ok(FrameExclusion::CurrentRow)
25548            }
25549            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
25550            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
25551            // Without this arm it fell to the catch-all, whose message
25552            // self-contradictingly listed GROUP as expected.
25553            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
25554                self.advance();
25555                Ok(FrameExclusion::Group)
25556            }
25557            Token::Group => {
25558                self.advance();
25559                Ok(FrameExclusion::Group)
25560            }
25561            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
25562                self.advance();
25563                Ok(FrameExclusion::Ties)
25564            }
25565            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
25566                self.advance();
25567                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
25568                    return Err(self.err(format!(
25569                        "expected OTHERS after EXCLUDE NO, got {:?}",
25570                        self.peek()
25571                    )));
25572                }
25573                self.advance();
25574                Ok(FrameExclusion::NoOthers)
25575            }
25576            other => Err(self.err(format!(
25577                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
25578            ))),
25579        }
25580    }
25581
25582    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
25583    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
25584    /// `UNBOUNDED FOLLOWING`.
25585    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
25586        // Interval-typed offset for a value-based RANGE frame over a
25587        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
25588        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
25589        // PRECEDING`.
25590        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
25591            let dir = self.expect_ident_like()?;
25592            return if dir.eq_ignore_ascii_case("preceding") {
25593                Ok(FrameBound::IntervalPreceding {
25594                    months,
25595                    days,
25596                    micros,
25597                })
25598            } else if dir.eq_ignore_ascii_case("following") {
25599                Ok(FrameBound::IntervalFollowing {
25600                    months,
25601                    days,
25602                    micros,
25603                })
25604            } else {
25605                Err(self.err(format!(
25606                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
25607                )))
25608            };
25609        }
25610        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
25611        if let Token::Integer(n) = *self.peek() {
25612            self.advance();
25613            let n: u64 = u64::try_from(n).map_err(|_| {
25614                self.err(format!(
25615                    "invalid frame offset {n} — expected non-negative integer"
25616                ))
25617            })?;
25618            let dir = self.expect_ident_like()?;
25619            return if dir.eq_ignore_ascii_case("preceding") {
25620                Ok(FrameBound::OffsetPreceding(n))
25621            } else if dir.eq_ignore_ascii_case("following") {
25622                Ok(FrameBound::OffsetFollowing(n))
25623            } else {
25624                Err(self.err(format!(
25625                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
25626                )))
25627            };
25628        }
25629        let first = self.expect_ident_like()?;
25630        if first.eq_ignore_ascii_case("unbounded") {
25631            let dir = self.expect_ident_like()?;
25632            return if dir.eq_ignore_ascii_case("preceding") {
25633                Ok(FrameBound::UnboundedPreceding)
25634            } else if dir.eq_ignore_ascii_case("following") {
25635                Ok(FrameBound::UnboundedFollowing)
25636            } else {
25637                Err(self.err(format!(
25638                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
25639                )))
25640            };
25641        }
25642        if first.eq_ignore_ascii_case("current") {
25643            let row = self.expect_ident_like()?;
25644            if !row.eq_ignore_ascii_case("row") {
25645                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
25646            }
25647            return Ok(FrameBound::CurrentRow);
25648        }
25649        Err(self.err(format!(
25650            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
25651        )))
25652    }
25653
25654    /// Detect and consume a leading interval offset in a frame bound —
25655    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
25656    /// `(months, days, micros)`. Leaves the cursor on the trailing
25657    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
25658    /// when the next tokens are not an interval offset.
25659    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
25660        // Shape A — `INTERVAL '1 day'`.
25661        if matches!(self.peek(), Token::Interval) {
25662            self.advance(); // INTERVAL
25663            let atom = self.parse_interval_atom()?;
25664            if let Expr::Literal(Literal::Interval {
25665                months,
25666                days,
25667                micros,
25668                ..
25669            }) = atom
25670            {
25671                return Ok(Some((months, days, micros)));
25672            }
25673            return Err(self.err("expected an interval literal in frame offset".to_string()));
25674        }
25675        // Shape B — `'1 day'::interval`. Look ahead for the exact
25676        // string / `::` / interval-target triple before committing.
25677        if let Token::String(text) = self.peek() {
25678            let target_is_interval = match self.tokens.get(self.pos + 2) {
25679                Some(Token::Interval) => true,
25680                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25681                _ => false,
25682            };
25683            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25684                && target_is_interval;
25685            if is_cast {
25686                let text = text.clone();
25687                self.advance(); // string
25688                self.advance(); // ::
25689                self.advance(); // interval
25690                let parts = parse_interval_text(&text).ok_or_else(|| {
25691                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25692                })?;
25693                return Ok(Some(parts));
25694            }
25695        }
25696        Ok(None)
25697    }
25698
25699    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25700        // v7.39.2 — MySQL's charset INTRODUCER: `_utf8mb4'x'`, `N'y'`,
25701        // `_binary'z'`. All three were `ERROR 1064 syntax error` here
25702        // and all three answer the literal on MySQL 9.7.2.
25703        //
25704        // It is not only syntax, which is why it waited for
25705        // `Expr::Collate`: measured, `_binary'A' = 'a'` is 0 on MySQL
25706        // because `_binary` makes the comparison byte-wise, while
25707        // `_utf8mb4'A' = _utf8mb4'a'` is 1. Accepting the syntax and
25708        // dropping the charset would have turned a hard error into a
25709        // silently wrong comparison — worse than the error it replaced.
25710        //
25711        // An UNKNOWN charset is NOT an introducer: MySQL answers
25712        // `Unknown column '_nosuch'`, because it parses as a column
25713        // reference followed by a string. So the table decides, and it
25714        // is the same table `SET NAMES` reads.
25715        //
25716        // A space is allowed between the two (`_utf8mb4 'x'`), which
25717        // falls out of asking the token stream rather than the bytes.
25718        if self.mysql_dialect
25719            && let Token::String(_) = self.peek()
25720        {
25721            let lower = first.to_ascii_lowercase();
25722            let charset = if lower == "n" {
25723                // `N'…'` is the national character set, which MySQL
25724                // documents as utf8 — utf8mb3 in 9.7.2's spelling.
25725                //
25726                // utf8mb3 and utf8mb4 both fold case in their default
25727                // collations, so nothing SPG can be asked distinguishes
25728                // the two here: an ablation swapping this to utf8mb4
25729                // reddens no pin. Recorded rather than implied — the
25730                // spelling follows MySQL's documentation, not a
25731                // measurement.
25732                Some("utf8mb3")
25733            } else {
25734                // No filter here: the lookup below IS the test for
25735                // "is this a charset". An ablation that removed a filter
25736                // in this spot reddened nothing, which is how the two
25737                // were found to be one check written twice.
25738                lower.strip_prefix('_')
25739            };
25740            if let Some(cs) = charset
25741                && let Some(collation) = crate::charset::charset_default_collation(cs)
25742            {
25743                let Token::String(body) = self.advance() else {
25744                    unreachable!("peeked a string");
25745                };
25746                return Ok(Expr::Collate {
25747                    expr: Box::new(Expr::Literal(Literal::String(body))),
25748                    collation: String::from(collation),
25749                });
25750            }
25751        }
25752        if matches!(self.peek(), Token::Dot) {
25753            self.advance();
25754            let name = self.expect_ident_like()?;
25755            // v7.14.0 — schema-qualified function call
25756            // `<schema>.<fn>(args)`. PG dumps emit
25757            // `pg_catalog.set_config(...)` in the preamble. SPG
25758            // is single-namespace: drop the schema prefix and
25759            // route the dispatch on the bare function name.
25760            if matches!(self.peek(), Token::LParen) {
25761                return self.finish_ident_atom(name);
25762            }
25763            return Ok(Expr::Column(ColumnName {
25764                qualifier: Some(first),
25765                name,
25766            }));
25767        }
25768        if matches!(self.peek(), Token::LParen) {
25769            self.advance();
25770            // `COUNT(*)` — special-cased here because `*` isn't a normal
25771            // expression token. Lower-case match on `first` since the lexer
25772            // folds identifiers.
25773            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25774                self.advance();
25775                if !matches!(self.peek(), Token::RParen) {
25776                    return Err(self.err(format!(
25777                        "expected ')' after COUNT(*), got {:?}",
25778                        self.peek()
25779                    )));
25780                }
25781                self.advance();
25782                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25783                let filter = self.parse_filter_clause()?;
25784                // v4.12: COUNT(*) OVER (...) — same window tail.
25785                let null_treatment = self.parse_null_treatment_modifier();
25786                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25787                    && s.eq_ignore_ascii_case("over")
25788                {
25789                    self.advance();
25790                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
25791                    return Ok(Expr::WindowFunction {
25792                        name: "count_star".into(),
25793                        args: Vec::new(),
25794                        partition_by,
25795                        order_by,
25796                        frame,
25797                        null_treatment,
25798                        filter,
25799                    });
25800                }
25801                if let Some(filter) = filter {
25802                    return Ok(Expr::AggregateOrdered {
25803                        call: Box::new(Expr::FunctionCall {
25804                            name: "count_star".into(),
25805                            args: Vec::new(),
25806                        }),
25807                        order_by: Vec::new(),
25808                        distinct: false,
25809                        filter: Some(filter),
25810                    });
25811                }
25812                return Ok(Expr::FunctionCall {
25813                    name: "count_star".into(),
25814                    args: Vec::new(),
25815                });
25816            }
25817            // Function call. PG-style: zero-or-more comma-separated args.
25818            let mut args = Vec::new();
25819            // v7.38 (read01, T14) — named-argument notation `argname => value`.
25820            // Names are collected in lock-step with `args` and resolved to
25821            // positional order after the loop (the AST stays positional).
25822            let mut arg_names: Vec<Option<String>> = Vec::new();
25823            let mut agg_order_by: Vec<OrderBy> = Vec::new();
25824            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25825            // seen, so the value arguments before it can be folded.
25826            let mut saw_separator = false;
25827            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25828            // v7.32 (round-29) — accept the dual `ALL` quantifier too
25829            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25830            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25831                self.advance();
25832                true
25833            } else if matches!(self.peek(), Token::All) {
25834                self.advance();
25835                false
25836            } else {
25837                false
25838            };
25839            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25840            // TIMESTAMPDIFF take a bare unit keyword as the first
25841            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25842            // bare type keyword (DATE / TIME / DATETIME); lower them
25843            // onto string literals so the evaluator sees plain text.
25844            if ((first.eq_ignore_ascii_case("timestampadd")
25845                || first.eq_ignore_ascii_case("timestampdiff"))
25846                && matches!(self.peek(), Token::Ident(u) if matches!(
25847                    u.to_ascii_lowercase().as_str(),
25848                    "microsecond" | "second" | "minute" | "hour" | "day"
25849                        | "week" | "month" | "quarter" | "year"
25850                )))
25851                || (first.eq_ignore_ascii_case("get_format")
25852                    && matches!(self.peek(), Token::Ident(u) if matches!(
25853                        u.to_ascii_lowercase().as_str(),
25854                        "date" | "time" | "datetime" | "timestamp"
25855                    )))
25856            {
25857                if let Token::Ident(u) = self.peek() {
25858                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25859                }
25860                self.advance();
25861                if matches!(self.peek(), Token::Comma) {
25862                    self.advance();
25863                }
25864            }
25865            // `ROW(a, b, …)` keyword constructor. Followed by a
25866            // comparison operator or [NOT] IN it joins the paren
25867            // row-constructor machinery (fieldwise parse-time
25868            // expansion); bare, it stays a `row` call the evaluator
25869            // renders as PG record text.
25870            if first.eq_ignore_ascii_case("row") {
25871                let mut row_items = Vec::new();
25872                if !matches!(self.peek(), Token::RParen) {
25873                    loop {
25874                        row_items.push(self.parse_expr(0)?);
25875                        match self.peek() {
25876                            Token::Comma => {
25877                                self.advance();
25878                            }
25879                            Token::RParen => break,
25880                            other => {
25881                                return Err(self.err(format!(
25882                                    "expected ',' or ')' in ROW(...), got {other:?}"
25883                                )));
25884                            }
25885                        }
25886                    }
25887                }
25888                self.advance(); // ')'
25889                let comparison_follows = matches!(
25890                    self.peek(),
25891                    Token::Eq
25892                        | Token::NotEq
25893                        | Token::Lt
25894                        | Token::LtEq
25895                        | Token::Gt
25896                        | Token::GtEq
25897                        | Token::In
25898                ) || (matches!(self.peek(), Token::Not)
25899                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25900                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25901                if comparison_follows && !row_items.is_empty() {
25902                    return self.parse_row_comparison_tail(row_items);
25903                }
25904                return Ok(Expr::FunctionCall {
25905                    name: String::from("row"),
25906                    args: row_items,
25907                });
25908            }
25909            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25910            // the parse-mode keyword introduces the source text. SPG
25911            // carries XML as text, so both modes lower to __xmlparse(expr)
25912            // which validates well-formedness and returns Value::Xml.
25913            if first.eq_ignore_ascii_case("xmlparse")
25914                && matches!(self.peek(), Token::Ident(kw)
25915                    if kw.eq_ignore_ascii_case("document")
25916                        || kw.eq_ignore_ascii_case("content"))
25917            {
25918                let mode = match self.advance() {
25919                    Token::Ident(kw) => kw.to_ascii_lowercase(),
25920                    _ => unreachable!("peeked an ident"),
25921                };
25922                let src = self.parse_expr(0)?;
25923                if !matches!(self.peek(), Token::RParen) {
25924                    return Err(self.err(format!(
25925                        "expected ')' to close XMLPARSE, got {:?}",
25926                        self.peek()
25927                    )));
25928                }
25929                self.advance();
25930                return Ok(Expr::FunctionCall {
25931                    name: String::from("__xmlparse"),
25932                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25933                });
25934            }
25935            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25936            // keyword introduces the element name (a bare or quoted
25937            // identifier), then optional content expressions. Lower to a
25938            // plain `xmlelement(name_text, content …)` call.
25939            if first.eq_ignore_ascii_case("xmlelement")
25940                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25941            {
25942                self.advance(); // consume NAME
25943                let elem_name = match self.peek().clone() {
25944                    Token::Ident(n) | Token::QuotedIdent(n) => {
25945                        self.advance();
25946                        n
25947                    }
25948                    other => {
25949                        return Err(self.err(format!(
25950                            "expected element name after XMLELEMENT NAME, got {other:?}"
25951                        )));
25952                    }
25953                };
25954                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25955                while matches!(self.peek(), Token::Comma) {
25956                    self.advance();
25957                    args.push(self.parse_expr(0)?);
25958                }
25959                if !matches!(self.peek(), Token::RParen) {
25960                    return Err(self.err(format!(
25961                        "expected ')' to close XMLELEMENT, got {:?}",
25962                        self.peek()
25963                    )));
25964                }
25965                self.advance();
25966                return Ok(Expr::FunctionCall {
25967                    name: String::from("xmlelement"),
25968                    args,
25969                });
25970            }
25971            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25972            // becomes a `<name>value</name>` element; a bare column infers its
25973            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25974            // v7.39.2 — MySQL's two CONVERT forms, neither of which parsed.
25975            // `CONVERT(expr USING cs)` was a syntax error at USING, and
25976            // `CONVERT(expr, CHAR)` was read as PostgreSQL's three-argument
25977            // `convert(bytea, src, dest)` and answered `column "char" does
25978            // not exist`. Both are casts in MySQL: measured on 9.7.2,
25979            // `CONVERT(0x41 USING utf8mb4)` and `CONVERT(0x41, CHAR)` are
25980            // both 'A', and `CONVERT(123, CHAR)` is '123'.
25981            //
25982            // The charset is checked against the same table the introducers
25983            // use, so an unknown one is refused rather than quietly ignored.
25984            if self.mysql_dialect
25985                && first.eq_ignore_ascii_case("convert")
25986                && !matches!(self.peek(), Token::RParen)
25987            {
25988                let save = self.pos;
25989                let inner = self.parse_expr(0)?;
25990                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
25991                    self.advance();
25992                    let cs = match self.peek().clone() {
25993                        Token::Ident(n) | Token::QuotedIdent(n) => {
25994                            self.advance();
25995                            n
25996                        }
25997                        other => {
25998                            return Err(self.err(alloc::format!(
25999                                "expected a charset after USING, got {other:?}"
26000                            )));
26001                        }
26002                    };
26003                    let lc = cs.to_ascii_lowercase();
26004                    if lc != "binary" && crate::charset::charset_default_collation(&lc).is_none() {
26005                        return Err(self.err(alloc::format!("unknown character set: '{cs}'")));
26006                    }
26007                    if !matches!(self.peek(), Token::RParen) {
26008                        return Err(self.err(alloc::format!(
26009                            "expected ')' after CONVERT … USING, got {:?}",
26010                            self.peek()
26011                        )));
26012                    }
26013                    self.advance();
26014                    let target = if lc == "binary" {
26015                        CastTarget::Named("binary".to_string())
26016                    } else {
26017                        CastTarget::Text
26018                    };
26019                    return self.finish_postfix_casts(Expr::Cast {
26020                        expr: alloc::boxed::Box::new(inner),
26021                        target,
26022                    });
26023                }
26024                if matches!(self.peek(), Token::Comma) {
26025                    self.advance();
26026                    // A type name here is MySQL's cast form; anything else
26027                    // (three string arguments) is PostgreSQL's `convert`,
26028                    // which keeps its own path.
26029                    if let Ok(target) = self.parse_cast_target()
26030                        && matches!(self.peek(), Token::RParen)
26031                    {
26032                        self.advance();
26033                        return self.finish_postfix_casts(Expr::Cast {
26034                            expr: alloc::boxed::Box::new(inner),
26035                            target,
26036                        });
26037                    }
26038                }
26039                self.pos = save;
26040            }
26041            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
26042                let mut args: Vec<Expr> = Vec::new();
26043                loop {
26044                    let val = self.parse_expr(0)?;
26045                    let name = if matches!(self.peek(), Token::As) {
26046                        self.advance();
26047                        match self.peek().clone() {
26048                            Token::Ident(n) | Token::QuotedIdent(n) => {
26049                                self.advance();
26050                                n
26051                            }
26052                            other => {
26053                                return Err(self.err(format!(
26054                                    "expected name after AS in XMLFOREST, got {other:?}"
26055                                )));
26056                            }
26057                        }
26058                    } else if let Expr::Column(c) = &val {
26059                        c.name.clone()
26060                    } else {
26061                        return Err(
26062                            self.err("XMLFOREST element without a column name needs AS".into())
26063                        );
26064                    };
26065                    args.push(Expr::Literal(Literal::String(name)));
26066                    args.push(val);
26067                    if matches!(self.peek(), Token::Comma) {
26068                        self.advance();
26069                    } else {
26070                        break;
26071                    }
26072                }
26073                if !matches!(self.peek(), Token::RParen) {
26074                    return Err(self.err(format!(
26075                        "expected ')' to close XMLFOREST, got {:?}",
26076                        self.peek()
26077                    )));
26078                }
26079                self.advance();
26080                return Ok(Expr::FunctionCall {
26081                    name: String::from("xmlforest"),
26082                    args,
26083                });
26084            }
26085            // SQL-standard `POSITION(sub IN str)` — lowers onto
26086            // strpos(str, sub). IN is the argument separator here,
26087            // so the needle parses with the IN-tail suppressed.
26088            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
26089                let saved = self.suppress_in_tail;
26090                self.suppress_in_tail = true;
26091                let needle = self.parse_expr(0);
26092                self.suppress_in_tail = saved;
26093                let needle = needle?;
26094                if matches!(self.peek(), Token::In) {
26095                    self.advance();
26096                    let haystack = self.parse_expr(0)?;
26097                    if !matches!(self.peek(), Token::RParen) {
26098                        return Err(self.err(format!(
26099                            "expected ')' to close POSITION, got {:?}",
26100                            self.peek()
26101                        )));
26102                    }
26103                    self.advance();
26104                    return Ok(Expr::FunctionCall {
26105                        name: String::from("strpos"),
26106                        args: alloc::vec![haystack, needle],
26107                    });
26108                }
26109                // position(sub, str) comma form (incl. bytea) —
26110                // hand the parsed first arg to the generic list.
26111                args.push(needle);
26112                if matches!(self.peek(), Token::Comma) {
26113                    self.advance();
26114                }
26115            }
26116            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
26117            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
26118            // plain comma forms TRIM(str) / TRIM(str, chars) keep
26119            // riding the generic argument list below.
26120            if first.eq_ignore_ascii_case("trim") {
26121                let mode = match self.peek() {
26122                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
26123                        self.advance();
26124                        Some("btrim")
26125                    }
26126                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
26127                        self.advance();
26128                        Some("ltrim")
26129                    }
26130                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
26131                        self.advance();
26132                        Some("rtrim")
26133                    }
26134                    _ => None,
26135                };
26136                if mode.is_some() || matches!(self.peek(), Token::From) {
26137                    // TRIM([mode] FROM str) — no strip-chars.
26138                    let chars = if matches!(self.peek(), Token::From) {
26139                        None
26140                    } else {
26141                        Some(self.parse_expr(0)?)
26142                    };
26143                    if !matches!(self.peek(), Token::From) {
26144                        return Err(self.err(format!(
26145                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
26146                            self.peek()
26147                        )));
26148                    }
26149                    self.advance();
26150                    let target = self.parse_expr(0)?;
26151                    if !matches!(self.peek(), Token::RParen) {
26152                        return Err(
26153                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
26154                        );
26155                    }
26156                    self.advance();
26157                    let mut trim_args = alloc::vec![target];
26158                    if let Some(c) = chars {
26159                        trim_args.push(c);
26160                    }
26161                    return Ok(Expr::FunctionCall {
26162                        name: String::from(mode.unwrap_or("btrim")),
26163                        args: trim_args,
26164                    });
26165                }
26166            }
26167            if !matches!(self.peek(), Token::RParen) {
26168                loop {
26169                    // v7.38 (read01, T14) — `argname => value` names this arg.
26170                    // v7.39 (read01 round 77) — `argname := value` is the same
26171                    // thing, and it is the spelling PG's own docs lead with. It
26172                    // was simply never lexed here, so every `f(x := 1)` died in
26173                    // the parser regardless of what `f` was.
26174                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
26175                        (
26176                            Token::Ident(n) | Token::QuotedIdent(n),
26177                            Some(Token::FatArrow | Token::ColonEq),
26178                        ) => {
26179                            let name = n.clone();
26180                            self.advance(); // name
26181                            self.advance(); // => / :=
26182                            Some(name)
26183                        }
26184                        _ => None,
26185                    };
26186                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
26187                    // array's elements into a variadic call's trailing args
26188                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
26189                    // reserved, so it arrives as a bare ident before the arg.
26190                    let is_variadic = this_name.is_none()
26191                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
26192                    if is_variadic {
26193                        self.advance();
26194                    }
26195                    let arg = self.parse_expr(0)?;
26196                    args.push(match &this_name {
26197                        // The callee's parameter names decide the slot, and a
26198                        // user function's live in the catalog. Carry the name
26199                        // to eval rather than guessing here.
26200                        Some(n) => Expr::NamedArg {
26201                            name: n.clone(),
26202                            expr: Box::new(arg),
26203                        },
26204                        None if is_variadic => Expr::Variadic(Box::new(arg)),
26205                        None => arg,
26206                    });
26207                    arg_names.push(this_name);
26208                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
26209                    // The `::` cast already worked; this lowers the
26210                    // function form onto the same Expr::Cast node.
26211                    if first.eq_ignore_ascii_case("cast")
26212                        && args.len() == 1
26213                        && matches!(self.peek(), Token::As)
26214                    {
26215                        self.advance();
26216                        let target = self.parse_cast_target()?;
26217                        if !matches!(self.peek(), Token::RParen) {
26218                            return Err(self.err(format!(
26219                                "expected ')' to close CAST, got {:?}",
26220                                self.peek()
26221                            )));
26222                        }
26223                        self.advance();
26224                        return Ok(Expr::Cast {
26225                            expr: Box::new(args.pop().expect("one arg")),
26226                            target,
26227                        });
26228                    }
26229                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
26230                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
26231                    // keywords; SPG's lexer makes them plain idents (so they'd be
26232                    // read as column refs). Lower the keyword to the string form
26233                    // the evaluator already accepts.
26234                    if first.eq_ignore_ascii_case("normalize")
26235                        && args.len() == 1
26236                        && matches!(self.peek(), Token::Comma)
26237                    {
26238                        let form = match self.tokens.get(self.pos + 1) {
26239                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
26240                                let up = f.to_ascii_uppercase();
26241                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
26242                            }
26243                            _ => None,
26244                        };
26245                        if let Some(up) = form {
26246                            self.advance(); // comma
26247                            self.advance(); // form keyword
26248                            args.push(Expr::Literal(Literal::String(up)));
26249                        }
26250                    }
26251                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
26252                    // form. Desugars to the comma-list shape evaluator already
26253                    // handles. Triggered after the first arg when the function
26254                    // name is substring / substr and the next token is FROM
26255                    // (a reserved keyword in PG; SPG also reserves it).
26256                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
26257                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
26258                    // internal __substring_similar(str, pat, esc) call.
26259                    if (first.eq_ignore_ascii_case("substring")
26260                        || first.eq_ignore_ascii_case("substr"))
26261                        && args.len() == 1
26262                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
26263                    {
26264                        self.advance(); // SIMILAR
26265                        let pattern = self.parse_expr(0)?;
26266                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
26267                        {
26268                            return Err(self.err(format!(
26269                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
26270                                self.peek()
26271                            )));
26272                        }
26273                        self.advance(); // ESCAPE
26274                        let esc = self.parse_expr(0)?;
26275                        if !matches!(self.peek(), Token::RParen) {
26276                            return Err(self.err(format!(
26277                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
26278                                self.peek()
26279                            )));
26280                        }
26281                        self.advance();
26282                        args.push(pattern);
26283                        args.push(esc);
26284                        return Ok(Expr::FunctionCall {
26285                            name: "__substring_similar".to_string(),
26286                            args,
26287                        });
26288                    }
26289                    if (first.eq_ignore_ascii_case("substring")
26290                        || first.eq_ignore_ascii_case("substr"))
26291                        && args.len() == 1
26292                        && matches!(self.peek(), Token::From | Token::For)
26293                    {
26294                        // `substring(str FROM pos [FOR len])`, or the FOR-only
26295                        // `substring(str FOR len)` which PG treats as FROM 1.
26296                        if matches!(self.peek(), Token::From) {
26297                            self.advance();
26298                            let start = self.parse_expr(0)?;
26299                            args.push(start);
26300                        } else {
26301                            args.push(Expr::Literal(Literal::Integer(1)));
26302                        }
26303                        if matches!(self.peek(), Token::For) {
26304                            self.advance();
26305                            let length = self.parse_expr(0)?;
26306                            args.push(length);
26307                        }
26308                        if !matches!(self.peek(), Token::RParen) {
26309                            return Err(self.err(format!(
26310                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
26311                                self.peek()
26312                            )));
26313                        }
26314                        self.advance();
26315                        return Ok(Expr::FunctionCall {
26316                            name: first.to_ascii_lowercase(),
26317                            args,
26318                        });
26319                    }
26320                    // PG `overlay(str PLACING repl FROM n [FOR len])`
26321                    // syntactic form. Desugars to the `overlay(str,
26322                    // repl, n[, len])` comma-list shape the evaluator
26323                    // already implements. `PLACING` is not a reserved
26324                    // token in SPG, so it arrives as a bare Ident.
26325                    if first.eq_ignore_ascii_case("overlay")
26326                        && args.len() == 1
26327                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
26328                    {
26329                        self.advance(); // consume PLACING
26330                        args.push(self.parse_expr(0)?); // replacement
26331                        if !matches!(self.peek(), Token::From) {
26332                            return Err(self.err(format!(
26333                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
26334                                self.peek()
26335                            )));
26336                        }
26337                        self.advance();
26338                        args.push(self.parse_expr(0)?); // start position
26339                        if matches!(self.peek(), Token::For) {
26340                            self.advance();
26341                            args.push(self.parse_expr(0)?); // length
26342                        }
26343                        if !matches!(self.peek(), Token::RParen) {
26344                            return Err(self.err(format!(
26345                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
26346                                self.peek()
26347                            )));
26348                        }
26349                        self.advance();
26350                        return Ok(Expr::FunctionCall {
26351                            name: String::from("overlay"),
26352                            args,
26353                        });
26354                    }
26355                    // `TRIM(chars FROM str)` — the keyword-less
26356                    // spelling lands here after the chars parse
26357                    // (the keyword forms return earlier).
26358                    if first.eq_ignore_ascii_case("trim")
26359                        && args.len() == 1
26360                        && matches!(self.peek(), Token::From)
26361                    {
26362                        self.advance();
26363                        let target = self.parse_expr(0)?;
26364                        if !matches!(self.peek(), Token::RParen) {
26365                            return Err(self.err(format!(
26366                                "expected ')' to close TRIM(chars FROM str), got {:?}",
26367                                self.peek()
26368                            )));
26369                        }
26370                        self.advance();
26371                        let chars = args.pop().expect("one arg");
26372                        return Ok(Expr::FunctionCall {
26373                            name: String::from("btrim"),
26374                            args: alloc::vec![target, chars],
26375                        });
26376                    }
26377                    // v7.24 (round-16 A) — aggregate-internal
26378                    // ordering: `array_agg(x ORDER BY y DESC NULLS
26379                    // LAST)`. Keys close the argument list.
26380                    if matches!(self.peek(), Token::Order) {
26381                        self.advance();
26382                        if !self.peek_is_by() {
26383                            return Err(self.err(format!(
26384                                "expected BY after ORDER in aggregate args, got {:?}",
26385                                self.peek()
26386                            )));
26387                        }
26388                        self.advance();
26389                        loop {
26390                            // v7.39 (round 691) — save/restore, the discipline this parser
26391                            // already uses around `pending_sample_preds`, so a subquery inside
26392                            // a key neither inherits nor leaks the channel.
26393                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
26394                            let saved_coll = self.order_key_collation.take();
26395                            let parsed = self.parse_expr(0);
26396                            self.in_order_by_key = saved_flag;
26397                            let collation =
26398                                core::mem::replace(&mut self.order_key_collation, saved_coll);
26399                            let expr = parsed?;
26400                            let desc = if matches!(self.peek(), Token::Desc) {
26401                                self.advance();
26402                                true
26403                            } else if matches!(self.peek(), Token::Asc) {
26404                                self.advance();
26405                                false
26406                            } else {
26407                                false
26408                            };
26409                            let nulls_first = self.parse_optional_nulls_placement()?;
26410                            agg_order_by.push(OrderBy {
26411                                expr,
26412                                desc,
26413                                nulls_first,
26414                                collation,
26415                            });
26416                            if matches!(self.peek(), Token::Comma) {
26417                                self.advance();
26418                            } else {
26419                                break;
26420                            }
26421                        }
26422                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
26423                        // follow the ORDER BY inside GROUP_CONCAT.
26424                        if self.consume_group_concat_separator(&mut args)? {
26425                            saw_separator = true;
26426                        }
26427                        if !matches!(self.peek(), Token::RParen) {
26428                            return Err(self.err(format!(
26429                                "expected ')' after aggregate ORDER BY, got {:?}",
26430                                self.peek()
26431                            )));
26432                        }
26433                        break;
26434                    }
26435                    // v7.39 (round 354, M12) — …or directly after the
26436                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
26437                    // own spelling of what PG passes as string_agg's second
26438                    // argument; it was a parse error, so every MySQL query
26439                    // that names its own separator failed outright.
26440                    if self.consume_group_concat_separator(&mut args)? {
26441                        saw_separator = true;
26442                        break;
26443                    }
26444                    match self.peek() {
26445                        Token::Comma => {
26446                            self.advance();
26447                        }
26448                        Token::RParen => break,
26449                        other => {
26450                            return Err(self.err(format!(
26451                                "expected ',' or ')' in function args, got {other:?}"
26452                            )));
26453                        }
26454                    }
26455                }
26456            }
26457            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
26458            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
26459            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
26460            // meaning a separator — that is what the explicit SEPARATOR
26461            // tail is for. Fold them into one `concat(...)` so the
26462            // aggregate keeps its single value argument.
26463            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
26464                let values = args.len() - usize::from(saw_separator);
26465                if values > 1 {
26466                    let sep_arg = if saw_separator { args.pop() } else { None };
26467                    let folded = Expr::FunctionCall {
26468                        name: "concat".to_string(),
26469                        args: core::mem::take(&mut args),
26470                    };
26471                    args.push(folded);
26472                    if let Some(sep) = sep_arg {
26473                        args.push(sep);
26474                    }
26475                }
26476            }
26477            self.advance(); // consume ')'
26478            // v7.39 (read01 round 77) — named arguments are NOT reordered here
26479            // any more. The parser has no catalog, so it could only ever resolve
26480            // the handful of `make_*` builtins whose parameter names were baked
26481            // into a table right here — every user function got
26482            // "does not support named arguments", though the catalog has been
26483            // storing its parameter names all along. Reordering happens in eval,
26484            // in one place, for builtins and user functions alike.
26485            // v7.32 (round-29) — ordered-set aggregate tail
26486            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
26487            // (percentile_cont / percentile_disc / mode). The sort spec
26488            // lands in the same `order_by` slot a decorated aggregate
26489            // uses; the executor dispatches on the function name. WITHIN
26490            // GROUP and an intra-argument ORDER BY are mutually
26491            // exclusive (PG rejects both).
26492            let within_group_order = self.parse_within_group_clause()?;
26493            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
26494                return Err(self.err(
26495                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
26496                        .into(),
26497                ));
26498            }
26499            let within_group_seen = !within_group_order.is_empty();
26500            let agg_order_by = if within_group_order.is_empty() {
26501                agg_order_by
26502            } else {
26503                within_group_order
26504            };
26505            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
26506            let filter = self.parse_filter_clause()?;
26507            // v4.12: window-function tail — `name(args) OVER (...)`.
26508            // Promotes the just-parsed FunctionCall into a
26509            // WindowFunction node carrying partition + order.
26510            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
26511            // / `RESPECT NULLS OVER (...)` between the closing paren
26512            // and `OVER`.
26513            let null_treatment = self.parse_null_treatment_modifier();
26514            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26515                && s.eq_ignore_ascii_case("over")
26516            {
26517                self.advance();
26518                // v7.39 (round 230) — PG implements neither modifier for a
26519                // windowed call and says so (0A000). Both used to be parsed
26520                // and then silently dropped here, so `count(DISTINCT v)
26521                // OVER (…)` quietly answered the non-distinct count.
26522                if agg_distinct {
26523                    return Err(
26524                        self.err("DISTINCT is not implemented for window functions".to_string())
26525                    );
26526                }
26527                if !agg_order_by.is_empty() {
26528                    // PG separates the two shapes that land here: a
26529                    // WITHIN GROUP call is an ordered-set aggregate and gets
26530                    // its own message naming the aggregate; a plain
26531                    // `agg(x ORDER BY y)` gets the generic one.
26532                    let msg = if within_group_seen {
26533                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
26534                    } else {
26535                        "aggregate ORDER BY is not implemented for window functions".to_string()
26536                    };
26537                    return Err(self.err(msg));
26538                }
26539                let (partition_by, order_by, frame) = self.parse_over_clause()?;
26540                return Ok(Expr::WindowFunction {
26541                    name: first,
26542                    args,
26543                    partition_by,
26544                    order_by,
26545                    frame,
26546                    null_treatment,
26547                    filter,
26548                });
26549            }
26550            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
26551                return Ok(Expr::AggregateOrdered {
26552                    call: Box::new(Expr::FunctionCall { name: first, args }),
26553                    order_by: agg_order_by,
26554                    distinct: agg_distinct,
26555                    filter,
26556                });
26557            }
26558            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
26559            // over TIMESTAMPTZ and has no timestamp overload, so a
26560            // timestamp argument is coerced on the way in and the answer
26561            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
26562            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
26563            // zone`. SPG answered `timestamp without time zone`, dropping
26564            // the offset from every rendering.
26565            //
26566            // Writing the coercion PG performs makes the existing
26567            // argument-driven typing (the one `date_trunc` uses) reach the
26568            // right answer, rather than teaching the type layer a second
26569            // rule. MySQL's DATE_ADD is a different function that returns
26570            // DATE or DATETIME, so this is PG-dialect only.
26571            //
26572            // Out-of-line because this sits on the RECURSIVE descent
26573            // frame: an inline block with locals here costs every nesting
26574            // level, and the suite's deep-nesting sentinel overflowed the
26575            // 512 KiB parser stack the moment one was added (round 430's
26576            // lesson, in the same shape).
26577            if !self.mysql_dialect {
26578                lift_date_add_arg_to_timestamptz(&first, &mut args);
26579            }
26580            return Ok(Expr::FunctionCall { name: first, args });
26581        }
26582        // v7.9.20 — SQL-standard parenless keyword expressions
26583        // (PG treats these as functions called without parens).
26584        // Resolve to a synthetic FunctionCall so the engine's
26585        // eval path reuses the existing function-call routing.
26586        // mailrs G3.
26587        let lc = first.to_ascii_lowercase();
26588        if matches!(
26589            lc.as_str(),
26590            "current_date"
26591                | "current_time"
26592                | "current_timestamp"
26593                | "localtimestamp"
26594                | "localtime"
26595                // v7.37.17 (17.6 siblings) — session-identity SQL-
26596                // standard parenless keywords. current_user /
26597                // session_user / user were already caught by the
26598                // pgwire canned-response shortcut but bare-select
26599                // in the embedded engine went through Expr::Column
26600                // and errored. Adding them here so the parser
26601                // resolves to a synthetic FunctionCall that reuses
26602                // the existing eval/functions.rs dispatch.
26603                | "current_user"
26604                | "session_user"
26605                | "current_role"
26606                | "current_catalog"
26607                | "current_schema"
26608                | "current_database"
26609                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
26610                | "system_user"
26611        ) {
26612            return Ok(Expr::FunctionCall {
26613                name: lc,
26614                args: Vec::new(),
26615            });
26616        }
26617        Ok(Expr::Column(ColumnName {
26618            qualifier: None,
26619            name: first,
26620        }))
26621    }
26622}
26623
26624/// v7.39 (round 522) — write the coercion PG's `date_add` /
26625/// `date_subtract` signature performs.
26626///
26627/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
26628/// timestamp argument is cast on the way in and the answer is
26629/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
26630/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
26631/// `timestamp without time zone`, dropping the offset from every
26632/// rendering of the result.
26633///
26634/// Writing the cast the signature implies lets the existing
26635/// argument-driven typing (the one `date_trunc` uses) reach the right
26636/// answer instead of teaching the type layer a second rule. MySQL's
26637/// DATE_ADD is a different function returning DATE or DATETIME, so the
26638/// caller applies this in PG dialect only.
26639///
26640/// A free function, and not a block at the call site, because the caller
26641/// is on the recursive-descent frame chain.
26642#[inline(never)]
26643fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
26644    if args.len() != 2
26645        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
26646    {
26647        return;
26648    }
26649    let base = args.remove(0);
26650    args.insert(
26651        0,
26652        Expr::Cast {
26653            expr: Box::new(base),
26654            target: CastTarget::Timestamptz,
26655        },
26656    );
26657}
26658
26659/// v6.8.2 — walk an expression tree and return the first column
26660/// reference's bare name. Used by `parse_create_index_stmt_after_create`
26661/// to derive `CreateIndexStatement.column` from an expression
26662/// key (so downstream planner code resolving a primary column
26663/// position keeps working with expression indexes). Returns
26664/// `None` when the expression has no column ref at all — caller
26665/// surfaces that as a parse error.
26666fn extract_first_column(expr: &Expr) -> Option<String> {
26667    match expr {
26668        Expr::Column(cn) => Some(cn.name.clone()),
26669        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
26670        Expr::Binary { lhs, rhs, .. } => {
26671            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
26672        }
26673        Expr::Unary { expr: e, .. } => extract_first_column(e),
26674        // v7.39 (read01 round 93) — a cast wraps its operand: a common
26675        // expression-index key is `lower(col::text)`, where the column
26676        // sits under the `::text` cast inside the function arg. Without
26677        // descending here the key was rejected as "references no column".
26678        Expr::Cast { expr: e, .. } => extract_first_column(e),
26679        // v7.39.2 — and a COLLATE wraps its operand the same way.
26680        // `CREATE INDEX rc ON t (c COLLATE "C" DESC)` stopped naming a
26681        // column the moment the clause became a node instead of being
26682        // absorbed, and the key was rejected as referencing none. This
26683        // is the shape the wildcard below silently produces, which is
26684        // why it is spelled out.
26685        Expr::Collate { expr: e, .. } => extract_first_column(e),
26686        _ => None,
26687    }
26688}
26689
26690fn maybe_not(expr: Expr, negated: bool) -> Expr {
26691    if negated {
26692        Expr::Unary {
26693            op: UnOp::Not,
26694            expr: Box::new(expr),
26695        }
26696    } else {
26697        expr
26698    }
26699}
26700
26701/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
26702/// things in the two dialects, and SPG read all three PG's way:
26703///
26704/// | token | PG (and SPG) | MySQL, measured |
26705/// |---|---|---|
26706/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
26707/// | `&&` | inet / array overlap | **AND** |
26708/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
26709///
26710/// `1 || 0` answering the string '10' on a MySQL session is a wrong
26711/// answer with no error, which is why they are routed here rather than
26712/// left to the shared table.
26713impl Parser {
26714    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
26715        if self.mysql_dialect {
26716            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
26717            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
26718            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
26719            if let Token::Ident(w) = tok
26720                && w.eq_ignore_ascii_case("div")
26721            {
26722                return Some((BinOp::IntDiv, 8));
26723            }
26724            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
26725            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
26726            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
26727            // there sits in operand position, not infix).
26728            if let Token::Ident(w) = tok
26729                && w.eq_ignore_ascii_case("mod")
26730            {
26731                return Some((BinOp::Mod, 8));
26732            }
26733            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
26734            // plain ident to the lexer. Its precedence sits between OR (1)
26735            // and AND (3) — hence rung 2, the slot freed by moving AND up.
26736            if let Token::Ident(w) = tok
26737                && w.eq_ignore_ascii_case("xor")
26738            {
26739                return Some((BinOp::LogicalXor, 2));
26740            }
26741            match tok {
26742                Token::Concat => return Some((BinOp::Or, 1)),
26743                // MySQL's `&&` is logical AND, sharing AND's rung (3).
26744                Token::InetOverlap => return Some((BinOp::And, 3)),
26745                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
26746                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
26747                _ => {}
26748            }
26749        }
26750        binop_from(tok)
26751    }
26752}
26753
26754// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
26755// (which sits strictly between OR and AND), every level from AND upward was
26756// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
26757// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
26758// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
26759// the *relative* order of every PG operator is unchanged by the shift.
26760fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
26761    let pair = match tok {
26762        Token::Or => (BinOp::Or, 1),
26763        Token::And => (BinOp::And, 3),
26764        Token::Eq => (BinOp::Eq, 5),
26765        Token::NotEq => (BinOp::NotEq, 5),
26766        Token::Lt => (BinOp::Lt, 5),
26767        Token::LtEq => (BinOp::LtEq, 5),
26768        Token::Gt => (BinOp::Gt, 5),
26769        Token::GtEq => (BinOp::GtEq, 5),
26770        // pgvector distance ops all sit on the same rung — tighter than
26771        // comparisons (5) so `col <-> v < threshold` parses correctly.
26772        Token::L2Distance => (BinOp::L2Distance, 6),
26773        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
26774        // comparison rung.
26775        Token::GeomParallel => (BinOp::GeomParallel, 5),
26776        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
26777        // comparison rung.
26778        Token::OverLeft => (BinOp::OverLeft, 5),
26779        Token::OverRight => (BinOp::OverRight, 5),
26780        Token::GeomPerp => (BinOp::GeomPerp, 5),
26781        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
26782        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
26783        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
26784        Token::InnerProduct => (BinOp::InnerProduct, 6),
26785        Token::CosineDistance => (BinOp::CosineDistance, 6),
26786        Token::Plus => (BinOp::Add, 7),
26787        Token::Minus => (BinOp::Sub, 7),
26788        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
26789        // binds every "other" operator (`||`, `|`, `&`, `#`, the
26790        // pgvector distances above) BETWEEN additive (7) and the
26791        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
26792        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
26793        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
26794        // ("matches PG conceptually" — the round-753 audit measured it
26795        // false; the old rung errored on `'a' || 1 + 1` with
26796        // `text + integer`). Same-level chains left-fold, as PG does.
26797        Token::Concat => (BinOp::Concat, 6),
26798        Token::Pipe => (BinOp::BitOr, 6),
26799        Token::Amp => (BinOp::BitAnd, 6),
26800        Token::Star => (BinOp::Mul, 8),
26801        Token::Slash => (BinOp::Div, 8),
26802        Token::Percent => (BinOp::Mod, 8),
26803        // v4.14: JSON path ops bind tighter than comparisons (5)
26804        // and additive (7) so `doc->'k' = 'v'` parses correctly.
26805        // Same rung as the multiplicative ops.
26806        Token::JsonGet => (BinOp::JsonGet, 8),
26807        Token::JsonGetText => (BinOp::JsonGetText, 8),
26808        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26809        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26810        Token::JsonContains => (BinOp::JsonContains, 8),
26811        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26812        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26813        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26814        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26815        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26816        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26817        // v7.12.2 — `@@` binds at the comparison rung (looser than
26818        // arithmetic, tighter than AND / OR). PG places `@@` at
26819        // the same precedence as `=` / `<`, so we follow.
26820        Token::TsMatch => (BinOp::TsMatch, 5),
26821        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26822        // PG places these at the comparison rung (same level as `=`),
26823        // so we follow.
26824        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26825        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26826        Token::InetContains => (BinOp::InetContains, 5),
26827        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26828        Token::InetOverlap => (BinOp::InetOverlap, 5),
26829        // v7.39 (round 508) — the geometric and pattern-order predicates
26830        // ride the comparison rung, as every other predicate does.
26831        Token::Intersects => (BinOp::Intersects, 5),
26832        Token::IsBelow => (BinOp::IsBelow, 5),
26833        Token::IsAbove => (BinOp::IsAbove, 5),
26834        Token::PatternLt => (BinOp::PatternLt, 5),
26835        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26836        Token::PatternGt => (BinOp::PatternGt, 5),
26837        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26838        // `@@@` is the old spelling of `@@` and means exactly it.
26839        Token::TsMatchOld => (BinOp::TsMatch, 5),
26840        _ => return None,
26841    };
26842    Some(pair)
26843}
26844
26845#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26846// `as f32` here is intentional: vector elements widen / narrow into f32 on
26847// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26848// past ~15 decimal digits — both are acceptable for a fixed-precision
26849// pgvector column.
26850/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26851/// implicit table alias and break trailing clauses. WITH lands
26852/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26853/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26854/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26855/// / VALUES / FOR / LATERAL — all of which would otherwise be
26856/// silently swallowed by `parse_optional_alias`.
26857fn is_alias_stopword(s: &str) -> bool {
26858    matches!(
26859        s.to_ascii_lowercase().as_str(),
26860        "with"
26861            | "on"
26862            | "where"
26863            | "having"
26864            | "group"
26865            | "order"
26866            | "limit"
26867            | "offset"
26868            | "union"
26869            | "except"
26870            | "intersect"
26871            | "returning"
26872            | "set"
26873            | "values"
26874            | "for"
26875            | "window"
26876            | "tablesample"
26877            | "lateral"
26878            | "left"
26879            | "right"
26880            | "inner"
26881            | "outer"
26882            | "full"
26883            | "cross"
26884            | "join"
26885            | "natural"
26886            | "using"
26887            | "fetch"
26888    )
26889}
26890
26891fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26892    match e {
26893        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26894        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26895        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26896        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26897        // so scale the divisor by hand instead of `f32::powi`.)
26898        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26899            let mut div = 1.0f32;
26900            for _ in 0..*scale {
26901                div *= 10.0;
26902            }
26903            Some(*unscaled as f32 / div)
26904        }
26905        Expr::Unary {
26906            op: UnOp::Neg,
26907            expr,
26908        } => extract_numeric_literal(expr).map(|x| -x),
26909        _ => None,
26910    }
26911}
26912
26913/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26914/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26915/// negative. Returns `None` if any pair fails to parse or no pair is found.
26916///
26917/// Recognised units (case-insensitive, optional trailing `s`):
26918/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26919/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26920/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26921/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26922/// (PG-canonical: DST and month-boundary semantics depend on this).
26923/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26924/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26925/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26926#[allow(clippy::cast_possible_truncation)]
26927fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26928    let mut months: i64 = 0;
26929    let mut days: i64 = 0;
26930    let mut micros: i64 = 0;
26931    let mut in_time = false;
26932    let mut num = alloc::string::String::new();
26933    for ch in rest.chars() {
26934        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26935            num.push(ch);
26936            continue;
26937        }
26938        if ch == 'T' || ch == 't' {
26939            if !num.is_empty() {
26940                return None;
26941            }
26942            in_time = true;
26943            continue;
26944        }
26945        let n: f64 = num.parse().ok()?;
26946        num.clear();
26947        match (ch, in_time) {
26948            ('Y' | 'y', false) => months += (n * 12.0) as i64,
26949            ('M', false) => months += n as i64,
26950            ('W' | 'w', false) => days += (n * 7.0) as i64,
26951            ('D' | 'd', false) => days += n as i64,
26952            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26953            ('M', true) => micros += (n * 60_000_000.0) as i64,
26954            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26955            _ => return None,
26956        }
26957    }
26958    if !num.is_empty() {
26959        return None;
26960    }
26961    Some((
26962        i32::try_from(months).ok()?,
26963        i32::try_from(days).ok()?,
26964        micros,
26965    ))
26966}
26967
26968/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26969/// leading `-` negates the whole value). Rejects date-like strings.
26970fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26971    let (neg, body) = match s.strip_prefix('-') {
26972        Some(b) => (true, b),
26973        None => (false, s),
26974    };
26975    let (y, m) = body.split_once('-')?;
26976    let years: i32 = y.parse().ok()?;
26977    let mons: i32 = m.parse().ok()?;
26978    if years < 0 || mons < 0 {
26979        return None;
26980    }
26981    let total = years.checked_mul(12)?.checked_add(mons)?;
26982    Some((if neg { -total } else { total }, 0, 0))
26983}
26984
26985/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26986/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26987fn parse_interval_clock(tok: &str) -> Option<i64> {
26988    let (neg, body) = match tok.strip_prefix('-') {
26989        Some(r) => (true, r),
26990        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26991    };
26992    let mut it = body.split(':');
26993    let h: i64 = it.next()?.parse().ok()?;
26994    let m: i64 = it.next()?.parse().ok()?;
26995    let s_tok = it.next().unwrap_or("0");
26996    if it.next().is_some() {
26997        return None;
26998    }
26999    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
27000        let sec: i64 = sec.parse().ok()?;
27001        let mut f = alloc::string::String::from(frac);
27002        while f.len() < 6 {
27003            f.push('0');
27004        }
27005        f.truncate(6);
27006        let fus: i64 = f.parse().ok()?;
27007        sec.checked_mul(1_000_000)?.checked_add(fus)?
27008    } else {
27009        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
27010    };
27011    let total = h
27012        .checked_mul(3_600_000_000)?
27013        .checked_add(m.checked_mul(60_000_000)?)?
27014        .checked_add(sec_us)?;
27015    Some(if neg { -total } else { total })
27016}
27017
27018/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
27019/// every spelling PG accepts (measured against live PG18.4, not guessed):
27020/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
27021/// Before this, the unit table matched long names only, with an ad-hoc
27022/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
27023/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
27024/// INTERVAL", and it had also grown arms for the debris that stripping leaves
27025/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
27026/// fractional) both read from this one table now.
27027fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
27028    let u = raw.to_ascii_lowercase();
27029    Some(match u.as_str() {
27030        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
27031            "microsecond"
27032        }
27033        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
27034            "millisecond"
27035        }
27036        "second" | "seconds" | "sec" | "secs" | "s" => "second",
27037        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
27038        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
27039        "day" | "days" | "d" => "day",
27040        "week" | "weeks" | "w" => "week",
27041        "month" | "months" | "mon" | "mons" => "month",
27042        "year" | "years" | "yr" | "yrs" | "y" => "year",
27043        "decade" | "decades" | "dec" | "decs" => "decade",
27044        "century" | "centuries" | "cent" | "c" => "century",
27045        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
27046        _ => return None,
27047    })
27048}
27049
27050/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
27051/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
27052#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27053pub(crate) enum IntervalField {
27054    Year,
27055    Month,
27056    Day,
27057    Hour,
27058    Minute,
27059    Second,
27060}
27061
27062/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
27063/// spellings aren't standard for the qualifier position, so only the singular
27064/// forms are accepted.
27065/// v7.39 (round 350, M7) — MySQL's interval units, measured against
27066/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
27067/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
27068/// take a `'1 2'` style literal — are not read here; they stay a parse
27069/// error rather than being silently misread.)
27070/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
27071///
27072/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
27073/// to do with a `@@` engine setting, and an unset one reads NULL rather
27074/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
27075/// were the same node and `SELECT @x` answered "Unknown system variable".)
27076/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
27077/// not see a session override — measured, after `SET autocommit=0`,
27078/// `@@global.autocommit` is still 1.
27079///
27080/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
27081/// the parser's nesting budget is tuned against, and building these
27082/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
27083/// wall `parse_left_right_atom` and friends were factored out for).
27084#[inline(never)]
27085fn variable_ref_atom(raw: &str) -> Expr {
27086    let user_var = !raw.starts_with("@@");
27087    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
27088    Expr::FunctionCall {
27089        name: String::from(if user_var {
27090            "__spg_user_var"
27091        } else {
27092            "__spg_session_var"
27093        }),
27094        args: alloc::vec![Expr::Literal(Literal::String(bare))],
27095    }
27096}
27097
27098fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
27099    let Token::Ident(s) = tok else { return None };
27100    Some(match () {
27101        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
27102        () if s.eq_ignore_ascii_case("second") => "second",
27103        () if s.eq_ignore_ascii_case("minute") => "minute",
27104        () if s.eq_ignore_ascii_case("hour") => "hour",
27105        () if s.eq_ignore_ascii_case("day") => "day",
27106        () if s.eq_ignore_ascii_case("week") => "week",
27107        () if s.eq_ignore_ascii_case("month") => "month",
27108        () if s.eq_ignore_ascii_case("quarter") => "quarter",
27109        () if s.eq_ignore_ascii_case("year") => "year",
27110        () => return None,
27111    })
27112}
27113
27114/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
27115/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
27116/// which constructs the value at run time. Only the slot the unit names
27117/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
27118/// slot the builtin has (months and fractional seconds respectively).
27119fn make_interval_call(qty: Expr, unit: &str) -> Expr {
27120    let zero = || Expr::Literal(Literal::Integer(0));
27121    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
27122        lhs: alloc::boxed::Box::new(qty.clone()),
27123        op,
27124        rhs: alloc::boxed::Box::new(by),
27125    };
27126    // (years, months, weeks, days, hours, mins, secs)
27127    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
27128    match unit {
27129        "year" => args[0] = qty,
27130        "quarter" => {
27131            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
27132        }
27133        "month" => args[1] = qty,
27134        "week" => args[2] = qty,
27135        "day" => args[3] = qty,
27136        "hour" => args[4] = qty,
27137        "minute" => args[5] = qty,
27138        "second" => args[6] = qty,
27139        // The builtin's seconds slot takes a fraction, so microseconds ride
27140        // it scaled down; the divisor is a NUMERIC literal so the division
27141        // stays exact rather than going through a float.
27142        "microsecond" => {
27143            args[6] = scaled(
27144                crate::ast::BinOp::Div,
27145                Expr::Literal(Literal::Numeric {
27146                    unscaled: 1_000_000,
27147                    scale: 0,
27148                }),
27149            );
27150        }
27151        _ => args[3] = qty,
27152    }
27153    Expr::FunctionCall {
27154        name: alloc::string::String::from("make_interval"),
27155        args,
27156    }
27157}
27158
27159/// `(count, unit)` → `(months, days, micros)`.
27160fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
27161    let n: i64 = count.trim().parse().ok()?;
27162    Some(match unit {
27163        "microsecond" => (0, 0, n),
27164        "second" => (0, 0, n.checked_mul(1_000_000)?),
27165        "minute" => (0, 0, n.checked_mul(60_000_000)?),
27166        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
27167        "day" => (0, i32::try_from(n).ok()?, 0),
27168        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
27169        "month" => (i32::try_from(n).ok()?, 0, 0),
27170        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
27171        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
27172        _ => return None,
27173    })
27174}
27175
27176fn interval_field_of(tok: &Token) -> Option<IntervalField> {
27177    let Token::Ident(s) = tok else { return None };
27178    Some(match () {
27179        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
27180        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
27181        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
27182        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
27183        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
27184        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
27185        () => return None,
27186    })
27187}
27188
27189/// v7.39 (read01 round 102) — interpret an interval literal under a field
27190/// qualifier. Returns `(months, days, micros)`.
27191///
27192/// * A single field applied to a bare number sets which unit the number means,
27193///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
27194///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
27195/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
27196/// * Every other range, and any literal a single field can't read as a plain
27197///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
27198///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
27199///   like PG, and the qualifier there only bounds precision.
27200fn interpret_qualified_interval(
27201    text: &str,
27202    (f1, f2): (IntervalField, Option<IntervalField>),
27203) -> Option<(i32, i32, i64)> {
27204    if let Some(f2) = f2 {
27205        if f1 == IntervalField::Year && f2 == IntervalField::Month {
27206            if let Some(m) = parse_year_month_literal(text) {
27207                return Some((m, 0, 0));
27208            }
27209        }
27210        return parse_interval_text(text);
27211    }
27212    // Single field: reinterpret a bare number; otherwise the default parse.
27213    let trimmed = text.trim();
27214    if let Ok(val) = trimmed.parse::<f64>() {
27215        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
27216        #[allow(clippy::cast_possible_truncation)]
27217        let whole = val as i64;
27218        #[allow(clippy::cast_possible_truncation)]
27219        let secs_micros = {
27220            let m = val * 1_000_000.0;
27221            if m >= 0.0 {
27222                (m + 0.5) as i64
27223            } else {
27224                (m - 0.5) as i64
27225            }
27226        };
27227        return Some(match f1 {
27228            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
27229            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
27230            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
27231            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
27232            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
27233            IntervalField::Second => (0, 0, secs_micros),
27234        });
27235    }
27236    parse_interval_text(text)
27237}
27238
27239/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
27240fn parse_year_month_literal(text: &str) -> Option<i32> {
27241    let t = text.trim();
27242    let (neg, body) = match t.strip_prefix('-') {
27243        Some(r) => (true, r),
27244        None => (false, t.strip_prefix('+').unwrap_or(t)),
27245    };
27246    let mut it = body.split('-');
27247    let years: i32 = it.next()?.trim().parse().ok()?;
27248    let months: i32 = match it.next() {
27249        Some(m) => m.trim().parse().ok()?,
27250        None => 0,
27251    };
27252    if it.next().is_some() {
27253        return None;
27254    }
27255    let total = years.checked_mul(12)?.checked_add(months)?;
27256    Some(if neg { -total } else { total })
27257}
27258
27259pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
27260    // v7.38.19 — the two infinities, answered as the three extreme
27261    // fields PostgreSQL itself puts on the wire for them:
27262    //
27263    //   COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
27264    //     … 7fffffffffffffff 7fffffff 7fffffff
27265    //
27266    // So no caller has to know the spelling — every one of them already
27267    // reads the three numbers, and `IntervalKind::from_fields` names
27268    // what they mean.
27269    //
27270    // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
27271    // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
27272    // infinity. Interval takes the full word, in any case.
27273    {
27274        let word = s.trim();
27275        let word = word.strip_prefix('@').map_or(word, str::trim);
27276        let (neg, body) = match word.strip_prefix('-') {
27277            Some(rest) => (true, rest.trim_start()),
27278            None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
27279        };
27280        if body.eq_ignore_ascii_case("infinity") {
27281            return Some(if neg {
27282                (i32::MIN, i32::MIN, i64::MIN)
27283            } else {
27284                (i32::MAX, i32::MAX, i64::MAX)
27285            });
27286        }
27287    }
27288    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
27289    // `@` is decorative; a trailing `ago` negates the whole interval.
27290    let mut trimmed = s.trim();
27291    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
27292    let mut negate = false;
27293    if let Some(rest) = trimmed
27294        .strip_suffix("ago")
27295        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
27296    {
27297        negate = true;
27298        trimmed = rest.trim();
27299    }
27300    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
27301        let (mo, d, us) = v?;
27302        if negate {
27303            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
27304        } else {
27305            Some((mo, d, us))
27306        }
27307    };
27308    let s = trimmed;
27309    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
27310    // are single tokens, not the `<n> <unit>` pair form handled below.
27311    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
27312        return finish(parse_iso8601_interval(rest));
27313    }
27314    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
27315        if let Some(iv) = parse_year_month_interval(trimmed) {
27316            return finish(Some(iv));
27317        }
27318    }
27319    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
27320    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
27321    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
27322    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
27323        if let Ok(n) = trimmed.parse::<i64>() {
27324            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
27325        }
27326        if let Ok(f) = trimmed.parse::<f64>() {
27327            if f.is_finite() {
27328                #[allow(clippy::cast_possible_truncation)]
27329                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
27330            }
27331        }
27332    }
27333    // v7.39 (round 243) — PG accepts the number and unit run together
27334    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
27335    // the `<n> <unit>` pair loop below sees them as two.
27336    let raw_parts: Vec<&str> = s.split_whitespace().collect();
27337    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
27338    for p in raw_parts {
27339        let boundary = p
27340            .char_indices()
27341            .find(|(i, c)| {
27342                *i > 0
27343                    && c.is_ascii_alphabetic()
27344                    && p[..*i]
27345                        .chars()
27346                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
27347                    && p[..*i].chars().any(|d| d.is_ascii_digit())
27348            })
27349            .map(|(i, _)| i);
27350        match boundary {
27351            Some(i) => {
27352                parts.push(&p[..i]);
27353                parts.push(&p[i..]);
27354            }
27355            None => parts.push(p),
27356        }
27357    }
27358    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
27359    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
27360    // remains is the `<n> <unit>` pair form handled below.
27361    let mut clock_us: i64 = 0;
27362    let mut had_clock = false;
27363    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
27364        clock_us = parse_interval_clock(parts[pos])?;
27365        parts.remove(pos);
27366        had_clock = true;
27367    }
27368    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
27369    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
27370    let mut lone_days: i32 = 0;
27371    if had_clock && parts.len() == 1 {
27372        if let Ok(n) = parts[0].parse::<i64>() {
27373            lone_days = i32::try_from(n).ok()?;
27374            parts.clear();
27375        }
27376    }
27377    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
27378        return None;
27379    }
27380    let mut months: i32 = 0;
27381    let mut days: i32 = lone_days;
27382    let mut micros: i64 = clock_us;
27383    let mut i = 0;
27384    while i < parts.len() {
27385        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
27386        if let Ok(n) = parts[i].parse::<i64>() {
27387            match unit_stripped {
27388                "microsecond" => micros = micros.checked_add(n)?,
27389                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
27390                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
27391                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
27392                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
27393                "day" => {
27394                    let n32 = i32::try_from(n).ok()?;
27395                    days = days.checked_add(n32)?;
27396                }
27397                "week" => {
27398                    let n32 = i32::try_from(n).ok()?;
27399                    days = days.checked_add(n32.checked_mul(7)?)?;
27400                }
27401                "month" => {
27402                    let n32 = i32::try_from(n).ok()?;
27403                    months = months.checked_add(n32)?;
27404                }
27405                "year" => {
27406                    let n32 = i32::try_from(n).ok()?;
27407                    months = months.checked_add(n32.checked_mul(12)?)?;
27408                }
27409                // v7.39 (read01 timestamp.c) — the larger calendar units.
27410                "decade" => {
27411                    let n32 = i32::try_from(n).ok()?;
27412                    months = months.checked_add(n32.checked_mul(120)?)?;
27413                }
27414                "century" => {
27415                    let n32 = i32::try_from(n).ok()?;
27416                    months = months.checked_add(n32.checked_mul(1200)?)?;
27417                }
27418                "millennium" => {
27419                    let n32 = i32::try_from(n).ok()?;
27420                    months = months.checked_add(n32.checked_mul(12000)?)?;
27421                }
27422                _ => return None,
27423            }
27424        } else if let Ok(f) = parts[i].parse::<f64>() {
27425            // Fractional units cascade down to the next-finer field the way
27426            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
27427            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
27428            // no_std: f64 has no trunc/fract/round methods, so do them with
27429            // casts (toward-zero) + explicit round-half-away-from-zero.
27430            #[allow(clippy::cast_possible_truncation)]
27431            fn round_i64(x: f64) -> i64 {
27432                if x >= 0.0 {
27433                    (x + 0.5) as i64
27434                } else {
27435                    (x - 0.5) as i64
27436                }
27437            }
27438            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27439            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
27440                const DAY_US: f64 = 86_400_000_000.0;
27441                let whole = d as i64; // truncates toward zero
27442                let frac = d - whole as f64;
27443                *days = days.checked_add(i32::try_from(whole).ok()?)?;
27444                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
27445                Some(())
27446            }
27447            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27448            match unit_stripped {
27449                "microsecond" => micros = micros.checked_add(round_i64(f))?,
27450                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
27451                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
27452                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
27453                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
27454                "day" => add_days_frac(&mut days, &mut micros, f)?,
27455                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
27456                "month" => {
27457                    let whole = f as i64;
27458                    months = months.checked_add(i32::try_from(whole).ok()?)?;
27459                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
27460                }
27461                "year" => {
27462                    let m = f * 12.0;
27463                    let whole = m as i64;
27464                    months = months.checked_add(i32::try_from(whole).ok()?)?;
27465                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
27466                }
27467                _ => return None,
27468            }
27469        } else {
27470            return None;
27471        }
27472        i += 2;
27473    }
27474    finish(Some((months, days, micros)))
27475}
27476
27477/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
27478/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
27479/// `interval` is intentionally absent (handled by its own parser arm).
27480/// Returns `None` for names that aren't sensible as a bare typed literal, so
27481/// the caller falls back to treating the ident as a column reference.
27482fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
27483    Some(match ident {
27484        "date" => CastTarget::Date,
27485        "timestamp" | "datetime" => CastTarget::Timestamp,
27486        "timestamptz" => CastTarget::Timestamptz,
27487        "bool" | "boolean" => CastTarget::Bool,
27488        "int" | "integer" | "int4" => CastTarget::Int,
27489        "bigint" | "int8" => CastTarget::BigInt,
27490        "float8" | "double precision" => CastTarget::Float,
27491        "uuid" => CastTarget::Uuid,
27492        "bytea" => CastTarget::Bytea,
27493        "json" => CastTarget::Json,
27494        "jsonb" => CastTarget::Jsonb,
27495        // Types without a dedicated CastTarget variant flow through the
27496        // generic Named path (engine resolves via column_type_to_data_type).
27497        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
27498        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
27499        | "money" | "bit" | "varbit"
27500        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
27501        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
27502        // Range / multirange types likewise.
27503        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
27504        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
27505        | "datemultirange" | "tsmultirange" | "tstzmultirange"
27506        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
27507        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
27508            CastTarget::Named(alloc::string::String::from(ident))
27509        }
27510        _ => return None,
27511    })
27512}
27513
27514/// v7.12.4 — map a bare type-name identifier (the form that
27515/// appears in a function arg list or RETURNS clause) to a
27516/// [`ColumnTypeName`]. Returns `None` for unknown / extension
27517/// types so the caller can preserve them as
27518/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
27519///
27520/// Subset of the full column-type grammar — we deliberately
27521/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
27522/// here because function-arg types in v7.12.4 are mostly the
27523/// bare form (`text`, `int`, `bytea`, …).
27524/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
27525/// than being `name TYPE`?
27526///
27527/// The multi-word spellings SQL allows for a bare argument type, each
27528/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
27529///
27530/// NOTE this list also exists in `spg-storage`, which computes the
27531/// signature key from the rendered argument text and has to reach the
27532/// same verdict. The two crates are siblings — neither depends on the
27533/// other — and each already carries its own table of type spellings
27534/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
27535/// there), so this follows the structure rather than inventing new
27536/// duplication. Recorded as V49.
27537pub fn is_multiword_type_phrase(phrase: &str) -> bool {
27538    let t = phrase.trim().to_ascii_lowercase();
27539    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
27540    matches!(
27541        base,
27542        "double precision"
27543            | "character varying"
27544            | "bit varying"
27545            | "timestamp with time zone"
27546            | "timestamp without time zone"
27547            | "time with time zone"
27548            | "time without time zone"
27549            | "national character"
27550            | "national character varying"
27551    )
27552}
27553
27554fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
27555    Some(match ident.to_ascii_lowercase().as_str() {
27556        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
27557        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
27558        "bigint" => ColumnTypeName::BigInt,
27559        "float" | "double" => ColumnTypeName::Float,
27560        // v7.39 (round 269) — real is 32-bit.
27561        "real" | "float4" => ColumnTypeName::Real,
27562        "text" => ColumnTypeName::Text,
27563        "bool" | "boolean" => ColumnTypeName::Bool,
27564        "date" => ColumnTypeName::Date,
27565        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
27566        "timestamptz" => ColumnTypeName::Timestamptz,
27567        "json" => ColumnTypeName::Json,
27568        "jsonb" => ColumnTypeName::Jsonb,
27569        "bytea" | "bytes" => ColumnTypeName::Bytes,
27570        "tsvector" => ColumnTypeName::TsVector,
27571        "tsquery" => ColumnTypeName::TsQuery,
27572        "uuid" => ColumnTypeName::Uuid,
27573        "interval" => ColumnTypeName::Interval,
27574        "time" => ColumnTypeName::Time,
27575        "year" => ColumnTypeName::Year,
27576        "timetz" => ColumnTypeName::TimeTz,
27577        "money" => ColumnTypeName::Money,
27578        _ => return None,
27579    })
27580}
27581
27582/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
27583/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
27584///
27585/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
27586/// / embedded SQL land in v7.12.5+):
27587///
27588/// ```text
27589///   body          := [ws] block [ws]
27590///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
27591///   stmt          := assign | return
27592///   assign        := assign_target := expr
27593///   assign_target := ( NEW | OLD ) . ident | ident
27594///   return        := RETURN ( NEW | OLD | NULL | expr )
27595/// ```
27596///
27597/// `expr` is parsed by recursing into the regular `Parser` — so a
27598/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
27599/// NEW.subject || ' ' || NEW.sender)` body shape works without
27600/// the body parser knowing what `to_tsvector` is.
27601///
27602/// Errors here cause the caller to fall back to
27603/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
27604/// successful, but the executor will refuse to invoke the
27605/// function with an "unparseable body" error.
27606/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
27607/// from the crate root as `spg_sql::parse_function_body`.
27608pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27609    parse_plpgsql_body(body)
27610}
27611
27612fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27613    // Use the regular lexer on the body text. The trailing
27614    // `END;` may or may not have a semicolon; the lexer treats
27615    // both forms identically.
27616    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
27617        message: alloc::format!("plpgsql body lex error: {e}"),
27618        token_pos: 0,
27619    })?;
27620    let mut parser = Parser::new(tokens);
27621    parser.parse_plpgsql_block()
27622}
27623
27624/// v7.39 (GUC) — the textual body of a SET value, for list joining.
27625fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
27626    match v {
27627        crate::ast::SetValue::String(s)
27628        | crate::ast::SetValue::Ident(s)
27629        | crate::ast::SetValue::Number(s) => s.clone(),
27630        crate::ast::SetValue::Default => "DEFAULT".into(),
27631    }
27632}
27633
27634/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
27635/// contains an aggregate call at ITS OWN query level (recursion stops at
27636/// sublink boundaries — a sublink's aggregates belong to the sublink).
27637/// Backs the "aggregate functions are not allowed in a recursive query's
27638/// recursive term" well-formedness check.
27639fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
27640    const AGG_NAMES: &[&str] = &[
27641        "count",
27642        "sum",
27643        "min",
27644        "max",
27645        "avg",
27646        "string_agg",
27647        "array_agg",
27648        "bool_and",
27649        "bool_or",
27650        "every",
27651        "any_value",
27652        "json_agg",
27653        "jsonb_agg",
27654        "json_object_agg",
27655        "jsonb_object_agg",
27656        "bit_and",
27657        "bit_or",
27658        "bit_xor",
27659        "var_pop",
27660        "var_samp",
27661        "variance",
27662        "stddev",
27663        "stddev_pop",
27664        "stddev_samp",
27665        "range_agg",
27666        "range_intersect_agg",
27667        "percentile_cont",
27668        "percentile_disc",
27669        "mode",
27670        "corr",
27671        "covar_pop",
27672        "covar_samp",
27673    ];
27674    match e {
27675        Expr::AggregateOrdered { .. } => true,
27676        Expr::FunctionCall { name, args } => {
27677            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
27678                || args.iter().any(expr_has_toplevel_aggregate)
27679        }
27680        Expr::NamedArg { expr, .. }
27681        | Expr::Variadic(expr)
27682        | Expr::Unary { expr, .. }
27683        | Expr::Cast { expr, .. }
27684        | Expr::IsNull { expr, .. }
27685        | Expr::FieldAccess { base: expr, .. }
27686        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
27687        Expr::Binary { lhs, rhs, .. } => {
27688            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
27689        }
27690        Expr::Like { expr, pattern, .. } => {
27691            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
27692        }
27693        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
27694        Expr::InList { expr, list, .. } => {
27695            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
27696        }
27697        Expr::ArraySubscript { target, index } => {
27698            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
27699        }
27700        Expr::ArraySlice { target, lo, hi } => {
27701            expr_has_toplevel_aggregate(target)
27702                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
27703                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
27704        }
27705        Expr::AnyAll { expr, array, .. } => {
27706            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
27707        }
27708        Expr::Case {
27709            operand,
27710            branches,
27711            else_branch,
27712        } => {
27713            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
27714                || branches
27715                    .iter()
27716                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
27717                || else_branch
27718                    .as_deref()
27719                    .is_some_and(expr_has_toplevel_aggregate)
27720        }
27721        // The outer-level operands of a sublink can aggregate; the sublink's
27722        // own body cannot leak its aggregates up here.
27723        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
27724        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
27725            row.iter().any(expr_has_toplevel_aggregate)
27726        }
27727        _ => false,
27728    }
27729}
27730
27731/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
27732/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
27733/// named table anywhere in its subtree. A plain FROM derived table is NOT a
27734/// sublink and is legal in a recursive term, so it is not walked here.
27735fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
27736    let mut exprs: Vec<&Expr> = Vec::new();
27737    for it in &s.items {
27738        if let crate::ast::SelectItem::Expr { expr, .. } = it {
27739            exprs.push(expr);
27740        }
27741    }
27742    if let Some(w) = &s.where_ {
27743        exprs.push(w);
27744    }
27745    if let Some(h) = &s.having {
27746        exprs.push(h);
27747    }
27748    if let Some(g) = &s.group_by {
27749        exprs.extend(g.iter());
27750    }
27751    if let Some(from) = &s.from {
27752        for j in &from.joins {
27753            if let Some(on) = &j.on {
27754                exprs.push(on);
27755            }
27756        }
27757    }
27758    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
27759}
27760
27761/// Does this expression contain a sublink whose subquery mentions `name`?
27762fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
27763    match e {
27764        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
27765        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
27766        Expr::InSubquery { expr, subquery, .. } => {
27767            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
27768        }
27769        Expr::RowInSubquery { row, subquery, .. } => {
27770            row.iter().any(|x| expr_sublink_mentions(x, name))
27771                || select_mentions_table(subquery, name)
27772        }
27773        Expr::RowCmpSubquery { row, subquery, .. } => {
27774            row.iter().any(|x| expr_sublink_mentions(x, name))
27775                || select_mentions_table(subquery, name)
27776        }
27777        Expr::NamedArg { expr, .. }
27778        | Expr::Variadic(expr)
27779        | Expr::Unary { expr, .. }
27780        | Expr::Cast { expr, .. }
27781        | Expr::IsNull { expr, .. }
27782        | Expr::FieldAccess { base: expr, .. }
27783        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
27784        Expr::Binary { lhs, rhs, .. } => {
27785            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
27786        }
27787        Expr::Like { expr, pattern, .. } => {
27788            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
27789        }
27790        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
27791            args.iter().any(|x| expr_sublink_mentions(x, name))
27792        }
27793        Expr::InList { expr, list, .. } => {
27794            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
27795        }
27796        Expr::ArraySubscript { target, index } => {
27797            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
27798        }
27799        Expr::ArraySlice { target, lo, hi } => {
27800            expr_sublink_mentions(target, name)
27801                || lo
27802                    .as_deref()
27803                    .is_some_and(|x| expr_sublink_mentions(x, name))
27804                || hi
27805                    .as_deref()
27806                    .is_some_and(|x| expr_sublink_mentions(x, name))
27807        }
27808        Expr::AnyAll { expr, array, .. } => {
27809            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
27810        }
27811        Expr::Case {
27812            operand,
27813            branches,
27814            else_branch,
27815        } => {
27816            operand
27817                .as_deref()
27818                .is_some_and(|x| expr_sublink_mentions(x, name))
27819                || branches
27820                    .iter()
27821                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
27822                || else_branch
27823                    .as_deref()
27824                    .is_some_and(|x| expr_sublink_mentions(x, name))
27825        }
27826        _ => false,
27827    }
27828}
27829
27830/// Does this SELECT (in full — FROM tables, derived tables, its own
27831/// sublinks, and union arms) mention the named table?
27832fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27833    if let Some(from) = &s.from {
27834        if from.primary.name.eq_ignore_ascii_case(name) {
27835            return true;
27836        }
27837        if let Some(sub) = &from.primary.lateral_subquery
27838            && select_mentions_table(sub, name)
27839        {
27840            return true;
27841        }
27842        for j in &from.joins {
27843            if j.table.name.eq_ignore_ascii_case(name) {
27844                return true;
27845            }
27846            if let Some(sub) = &j.table.lateral_subquery
27847                && select_mentions_table(sub, name)
27848            {
27849                return true;
27850            }
27851        }
27852    }
27853    if select_has_self_ref_in_sublink(s, name) {
27854        return true;
27855    }
27856    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27857}
27858
27859/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27860/// row count, the way PG evaluates one before applying it.
27861///
27862/// `None` = not a constant (a column, a subquery, a function call).
27863/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27864/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27865/// All wordings were read off live PG 18.4.
27866fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27867    use crate::ast::{BinOp, Expr, Literal, UnOp};
27868    match e {
27869        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27870        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27871            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27872        }
27873        // PG coerces a string by its CONTENT, and fails on the value.
27874        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27875            |_| {
27876                Err(alloc::format!(
27877                    "invalid input syntax for type bigint: \"{t}\""
27878                ))
27879            },
27880            |n| Ok(i128::from(n)),
27881        )),
27882        Expr::Literal(Literal::Bool(_)) => Some(Err(
27883            "argument of {L} must be type bigint, not type boolean".into(),
27884        )),
27885        Expr::Unary {
27886            op: UnOp::Neg,
27887            expr,
27888        } => match fold_limit_constant(expr)? {
27889            Ok(v) => Some(Ok(-v)),
27890            e @ Err(_) => Some(e),
27891        },
27892        Expr::Binary { lhs, op, rhs } => {
27893            let a = match fold_limit_constant(lhs)? {
27894                Ok(v) => v,
27895                e @ Err(_) => return Some(e),
27896            };
27897            let b = match fold_limit_constant(rhs)? {
27898                Ok(v) => v,
27899                e @ Err(_) => return Some(e),
27900            };
27901            let out = match op {
27902                BinOp::Add => a.checked_add(b),
27903                BinOp::Sub => a.checked_sub(b),
27904                BinOp::Mul => a.checked_mul(b),
27905                BinOp::Div if b != 0 => a.checked_div(b),
27906                BinOp::Div => return Some(Err("division by zero".into())),
27907                BinOp::Mod if b != 0 => a.checked_rem(b),
27908                BinOp::Mod => return Some(Err("division by zero".into())),
27909                _ => return None,
27910            };
27911            // PG evaluates the arithmetic in the operand's own type, so an
27912            // int-by-int product that leaves int range fails there — before
27913            // the row count is ever looked at.
27914            match out {
27915                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27916                    Some(Err("integer out of range".into()))
27917                }
27918                Some(v) => Some(Ok(v)),
27919                None => Some(Err("integer out of range".into())),
27920            }
27921        }
27922        _ => None,
27923    }
27924}
27925
27926/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27927/// cast, which is what makes `LIMIT 2.5` keep three rows.
27928fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27929    if scale == 0 {
27930        return unscaled;
27931    }
27932    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27933        return 0;
27934    };
27935    let neg = unscaled < 0;
27936    let mag = unscaled.unsigned_abs() as i128;
27937    let rounded = (mag + div / 2) / div;
27938    if neg { -rounded } else { rounded }
27939}
27940
27941#[cfg(test)]
27942mod tests {
27943    use super::*;
27944    use alloc::string::ToString;
27945
27946    fn parse(s: &str) -> Statement {
27947        parse_statement(s).expect("parse ok")
27948    }
27949
27950    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27951    // `tables`, `partition`, etc. are unreserved keywords per PG's
27952    // `pg_get_keywords()` and MUST be usable as column / table /
27953    // alias names. Pre-T4 every drop-in user whose schema had one
27954    // of these as a column name (sentori events.release, mailrs
27955    // messages.index in some forks) blew the parser up at CREATE
27956    // TABLE time with "expected identifier, got Release". The
27957    // generalisation lives in `unreserved_keyword_text` + the
27958    // `expect_ident_like` and `parse_atom` arms that consult it.
27959    #[test]
27960    fn release_usable_as_column_name_in_create_table() {
27961        let stmt =
27962            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27963        if let Statement::CreateTable(t) = stmt {
27964            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27965            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27966        } else {
27967            panic!("expected CreateTable");
27968        }
27969    }
27970
27971    #[test]
27972    fn release_usable_as_column_ref_in_select_projection() {
27973        // The sentori `0003_partition_events.sql` INSERT-SELECT
27974        // walk references `release` in both column lists; the
27975        // projection-side use exercises `parse_atom`'s relaxed
27976        // identifier set.
27977        parse("SELECT id, release, payload FROM events WHERE id = 1");
27978    }
27979
27980    #[test]
27981    fn release_usable_as_column_ref_in_insert_column_list() {
27982        // INSERT INTO t (id, release, payload) VALUES (…)
27983        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27984    }
27985
27986    #[test]
27987    fn alter_column_drop_not_null_uses_keyword_drop_token() {
27988        // Sentori `0013_audit_tombstone.sql` issues
27989        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27990        // emits Token::Drop (not Ident("drop")); the parser must
27991        // accept both in the ALTER COLUMN sub-dispatch.
27992        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27993    }
27994
27995    #[test]
27996    fn create_index_accepts_parenthesised_expression_key() {
27997        // sentori `0040_events_bundle_idx.sql` shape — JSONB
27998        // expression index. Pre-T4 the parser bailed at the
27999        // inner `(` with "expected column ident or expression,
28000        // got LParen". The Token::LParen arm in CREATE INDEX
28001        // routes through the expression parser instead.
28002        parse(
28003            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
28004             ON events ((payload->'bundle'->>'id'))",
28005        );
28006    }
28007
28008    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
28009    // surface as parse errors, never stack overflows (embed hosts
28010    // abort on overflow).
28011    /// The nesting budget is a COUNT; what it has to fit inside is a
28012    /// number of BYTES, and only one of those two is stable across
28013    /// compiler versions. Round 847 measured 30,336 bytes per level
28014    /// after a toolchain move, which puts 64 levels at 1.94 MB and
28015    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
28016    /// aborted instead of erroring, which is precisely the outcome it
28017    /// exists to rule out.
28018    ///
28019    /// So the budget is metered rather than assumed. The ceiling leaves
28020    /// the depth SPG advertises fitting in a default 2 MiB thread with
28021    /// room to spare, in the debug build, where frames are widest.
28022    #[test]
28023    fn nesting_frame_cost_stays_under_ceiling() {
28024        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
28025        // thread keeps a margin for whatever called the parser.
28026        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
28027
28028        frame_meter::reset();
28029        let depth = frame_meter::SAMPLE_HI + 8;
28030        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28031        parse(&sql);
28032
28033        let per_level = frame_meter::bytes_per_level();
28034        {
28035            extern crate std;
28036            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
28037        }
28038        assert!(
28039            per_level <= CEILING,
28040            "{per_level} bytes per nesting level exceeds {CEILING}; \
28041             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
28042             in parse_expr_inner / parse_unary rather than lowering the \
28043             depth or widening the stack.",
28044            per_level * MAX_NEST_DEPTH
28045        );
28046    }
28047
28048    #[test]
28049    fn nesting_budget_errors_cleanly() {
28050        let depth = MAX_NEST_DEPTH + 50;
28051        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28052        let err = parse_statement(&sql).expect_err("must reject");
28053        assert!(err.message.contains("nests deeper"), "{err:?}");
28054        // Within budget still parses.
28055        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
28056        parse(&sql);
28057    }
28058
28059    #[test]
28060    fn binary_chain_budget_errors_cleanly() {
28061        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
28062        let err = parse_statement(&sql).expect_err("must reject");
28063        assert!(err.message.contains("chained binary"), "{err:?}");
28064        // Within budget still parses (chain depth ≤ budget is safe
28065        // for recursive eval/drop on 2 MiB stacks).
28066        let sql = format!("SELECT 1{}", " + 1".repeat(200));
28067        parse(&sql);
28068    }
28069
28070    #[test]
28071    fn in_list_unaffected_by_chain_budget() {
28072        // Flat InList: 20k elements parse fine and stay flat.
28073        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
28074        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
28075        let Statement::Select(s) = parse(&sql) else {
28076            panic!("expected select")
28077        };
28078        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
28079            panic!("expected flat InList, got {:?}", s.where_)
28080        };
28081        assert_eq!(list.len(), 20_000);
28082        assert!(!negated);
28083    }
28084
28085    fn lit_int(n: i64) -> Expr {
28086        Expr::Literal(Literal::Integer(n))
28087    }
28088
28089    fn col(name: &str) -> Expr {
28090        Expr::Column(ColumnName {
28091            qualifier: None,
28092            name: name.into(),
28093        })
28094    }
28095
28096    #[test]
28097    fn select_single_integer() {
28098        let s = parse("SELECT 1");
28099        let Statement::Select(s) = s else {
28100            panic!("expected SELECT")
28101        };
28102        assert_eq!(s.items.len(), 1);
28103        assert!(s.from.is_none());
28104        assert!(s.where_.is_none());
28105    }
28106
28107    #[test]
28108    fn select_multiple_literal_kinds() {
28109        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
28110        let Statement::Select(s) = s else {
28111            panic!("expected SELECT")
28112        };
28113        assert_eq!(s.items.len(), 5);
28114    }
28115
28116    #[test]
28117    fn select_wildcard_from_table() {
28118        let s = parse("SELECT * FROM users");
28119        let Statement::Select(s) = s else {
28120            panic!("expected SELECT")
28121        };
28122        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
28123        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
28124    }
28125
28126    #[test]
28127    fn select_with_table_alias() {
28128        let s = parse("SELECT * FROM users AS u");
28129        let Statement::Select(s) = s else {
28130            panic!("expected SELECT")
28131        };
28132        let t = &s.from.as_ref().unwrap().primary;
28133        assert_eq!(t.name, "users");
28134        assert_eq!(t.alias.as_deref(), Some("u"));
28135    }
28136
28137    #[test]
28138    fn select_with_where_eq() {
28139        let s = parse("SELECT a FROM t WHERE a = 1");
28140        let Statement::Select(s) = s else {
28141            panic!("expected SELECT")
28142        };
28143        let w = s.where_.unwrap();
28144        assert_eq!(
28145            w,
28146            Expr::Binary {
28147                lhs: Box::new(col("a")),
28148                op: BinOp::Eq,
28149                rhs: Box::new(lit_int(1)),
28150            }
28151        );
28152    }
28153
28154    #[test]
28155    fn arithmetic_precedence() {
28156        let s = parse("SELECT 1 + 2 * 3");
28157        let Statement::Select(s) = s else {
28158            panic!("expected SELECT")
28159        };
28160        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28161            panic!("wildcard?")
28162        };
28163        assert_eq!(
28164            expr,
28165            &Expr::Binary {
28166                lhs: Box::new(lit_int(1)),
28167                op: BinOp::Add,
28168                rhs: Box::new(Expr::Binary {
28169                    lhs: Box::new(lit_int(2)),
28170                    op: BinOp::Mul,
28171                    rhs: Box::new(lit_int(3)),
28172                }),
28173            }
28174        );
28175    }
28176
28177    #[test]
28178    fn parentheses_override_precedence() {
28179        let s = parse("SELECT (1 + 2) * 3");
28180        let Statement::Select(s) = s else {
28181            panic!("expected SELECT")
28182        };
28183        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28184            panic!()
28185        };
28186        assert_eq!(
28187            expr,
28188            &Expr::Binary {
28189                lhs: Box::new(Expr::Binary {
28190                    lhs: Box::new(lit_int(1)),
28191                    op: BinOp::Add,
28192                    rhs: Box::new(lit_int(2)),
28193                }),
28194                op: BinOp::Mul,
28195                rhs: Box::new(lit_int(3)),
28196            }
28197        );
28198    }
28199
28200    #[test]
28201    fn not_binds_below_comparison() {
28202        // `NOT a = 1` should parse as `NOT (a = 1)`.
28203        let s = parse("SELECT NOT a = 1 FROM t");
28204        let Statement::Select(s) = s else {
28205            panic!("expected SELECT")
28206        };
28207        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28208            panic!()
28209        };
28210        assert_eq!(
28211            expr,
28212            &Expr::Unary {
28213                op: UnOp::Not,
28214                expr: Box::new(Expr::Binary {
28215                    lhs: Box::new(col("a")),
28216                    op: BinOp::Eq,
28217                    rhs: Box::new(lit_int(1)),
28218                }),
28219            }
28220        );
28221    }
28222
28223    #[test]
28224    fn unary_minus_binds_above_multiplication() {
28225        // `-a * 2` should be `(-a) * 2`.
28226        let s = parse("SELECT -a * 2 FROM t");
28227        let Statement::Select(s) = s else {
28228            panic!("expected SELECT")
28229        };
28230        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28231            panic!()
28232        };
28233        assert_eq!(
28234            expr,
28235            &Expr::Binary {
28236                lhs: Box::new(Expr::Unary {
28237                    op: UnOp::Neg,
28238                    expr: Box::new(col("a")),
28239                }),
28240                op: BinOp::Mul,
28241                rhs: Box::new(lit_int(2)),
28242            }
28243        );
28244    }
28245
28246    #[test]
28247    fn qualified_column() {
28248        let s = parse("SELECT t.col FROM t");
28249        let Statement::Select(s) = s else {
28250            panic!("expected SELECT")
28251        };
28252        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28253            panic!()
28254        };
28255        assert_eq!(
28256            expr,
28257            &Expr::Column(ColumnName {
28258                qualifier: Some("t".into()),
28259                name: "col".into()
28260            })
28261        );
28262    }
28263
28264    #[test]
28265    fn select_item_alias_with_as() {
28266        let s = parse("SELECT a AS y FROM t");
28267        let Statement::Select(s) = s else {
28268            panic!("expected SELECT")
28269        };
28270        let SelectItem::Expr { alias, .. } = &s.items[0] else {
28271            panic!()
28272        };
28273        assert_eq!(alias.as_deref(), Some("y"));
28274    }
28275
28276    #[test]
28277    fn trailing_semicolon_accepted() {
28278        let s = parse("SELECT 1;");
28279        let Statement::Select(s) = s else {
28280            panic!("expected SELECT")
28281        };
28282        assert_eq!(s.items.len(), 1);
28283    }
28284
28285    #[test]
28286    fn boolean_chain_with_and_or_not() {
28287        // (NOT a) OR (b AND (NOT c))
28288        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
28289        let Statement::Select(s) = s else {
28290            panic!("expected SELECT")
28291        };
28292        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28293            panic!()
28294        };
28295        let expected = Expr::Binary {
28296            lhs: Box::new(Expr::Unary {
28297                op: UnOp::Not,
28298                expr: Box::new(col("a")),
28299            }),
28300            op: BinOp::Or,
28301            rhs: Box::new(Expr::Binary {
28302                lhs: Box::new(col("b")),
28303                op: BinOp::And,
28304                rhs: Box::new(Expr::Unary {
28305                    op: UnOp::Not,
28306                    expr: Box::new(col("c")),
28307                }),
28308            }),
28309        };
28310        assert_eq!(expr, &expected);
28311    }
28312
28313    #[test]
28314    fn empty_input_errors() {
28315        // v7.14.0 — pg_dump preambles emit several comment-only
28316        // / blank-line statements that collapse to Statement::
28317        // Empty rather than a parse error. The old "SELECT in
28318        // message" assertion is stale; verify the new contract:
28319        // empty / whitespace / comment-only input parses to
28320        // Statement::Empty.
28321        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
28322        assert!(matches!(
28323            parse_statement("  \n\t ").unwrap(),
28324            Statement::Empty
28325        ));
28326        // Sanity: malformed-but-non-empty still errors.
28327        assert!(parse_statement("SELECT FROM WHERE").is_err());
28328    }
28329
28330    #[test]
28331    fn unmatched_paren_errors() {
28332        assert!(parse_statement("SELECT (1 + 2").is_err());
28333    }
28334
28335    #[test]
28336    fn display_round_trip_simple_select() {
28337        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
28338        let text = original.to_string();
28339        let again = parse_statement(&text).expect("re-parse");
28340        assert_eq!(original, again);
28341    }
28342
28343    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
28344
28345    #[test]
28346    fn create_table_single_column() {
28347        let s = parse("CREATE TABLE foo (a INT)");
28348        let Statement::CreateTable(c) = s else {
28349            panic!("expected CreateTable")
28350        };
28351        assert_eq!(c.name, "foo");
28352        assert_eq!(c.columns.len(), 1);
28353        assert_eq!(c.columns[0].name, "a");
28354        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28355        assert!(c.columns[0].nullable);
28356    }
28357
28358    #[test]
28359    fn create_table_multi_column_with_not_null_mix() {
28360        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
28361        let Statement::CreateTable(c) = s else {
28362            panic!()
28363        };
28364        assert_eq!(c.columns.len(), 4);
28365        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28366        assert!(!c.columns[0].nullable);
28367        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
28368        assert!(c.columns[1].nullable);
28369        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
28370        assert!(!c.columns[2].nullable);
28371        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
28372    }
28373
28374    #[test]
28375    fn create_table_bigint_supported() {
28376        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
28377        let Statement::CreateTable(c) = s else {
28378            panic!()
28379        };
28380        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
28381    }
28382
28383    #[test]
28384    fn create_table_vector_default_is_f32() {
28385        let s = parse("CREATE TABLE t (v VECTOR(128))");
28386        let Statement::CreateTable(c) = s else {
28387            panic!()
28388        };
28389        assert_eq!(
28390            c.columns[0].ty,
28391            ColumnTypeName::Vector {
28392                dim: 128,
28393                encoding: VecEncoding::F32,
28394            },
28395        );
28396    }
28397
28398    #[test]
28399    fn create_table_vector_using_sq8() {
28400        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
28401        // Case-insensitive on both `USING` and the encoding name.
28402        for sql in [
28403            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
28404            "CREATE TABLE t (v VECTOR(128) using sq8)",
28405        ] {
28406            let s = parse(sql);
28407            let Statement::CreateTable(c) = s else {
28408                panic!()
28409            };
28410            assert_eq!(
28411                c.columns[0].ty,
28412                ColumnTypeName::Vector {
28413                    dim: 128,
28414                    encoding: VecEncoding::Sq8,
28415                },
28416                "{sql}",
28417            );
28418        }
28419    }
28420
28421    #[test]
28422    fn create_table_vector_using_unknown_errors() {
28423        // v7.16.1 — the inline `USING <encoding>` shape on
28424        // CREATE TABLE column defs was withdrawn before
28425        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
28426        // (col vector_<metric>_ops)`; the parser now rejects
28427        // USING at column-list position with a clearer
28428        // "expected ',' or ')'" message. Test asserts the
28429        // current rejection, not the old "unknown vector
28430        // encoding" string.
28431        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
28432        assert!(
28433            err.message.contains("USING")
28434                || err.message.contains("using")
28435                || err.message.contains("')'")
28436                || err.message.contains("','"),
28437            "expected USING/column-list rejection, got: {}",
28438            err.message
28439        );
28440    }
28441
28442    #[test]
28443    fn vector_using_sq8_display_roundtrips() {
28444        // The Display impl must produce text that re-parses to the
28445        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
28446        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
28447        let Statement::CreateTable(c) = s else {
28448            panic!()
28449        };
28450        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
28451    }
28452
28453    #[test]
28454    fn parser_recognises_placeholders() {
28455        use crate::ast::{Expr, SelectItem, Statement};
28456        // $N in expression position parses as Expr::Placeholder(N).
28457        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
28458        let Statement::Select(sel) = s else { panic!() };
28459        assert!(matches!(
28460            sel.items[0],
28461            SelectItem::Expr {
28462                expr: Expr::Placeholder(1),
28463                alias: None
28464            }
28465        ));
28466        // $2 + 1
28467        let SelectItem::Expr {
28468            expr: Expr::Binary { lhs, rhs, .. },
28469            ..
28470        } = &sel.items[1]
28471        else {
28472            panic!()
28473        };
28474        assert!(matches!(**lhs, Expr::Placeholder(2)));
28475        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
28476        // WHERE x = $3
28477        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
28478            panic!()
28479        };
28480        assert!(matches!(**rhs, Expr::Placeholder(3)));
28481    }
28482
28483    #[test]
28484    fn parser_rejects_dollar_zero() {
28485        // $0 is not valid in PG; the lexer rejects it.
28486        assert!(parse_statement("SELECT $0").is_err());
28487    }
28488
28489    #[test]
28490    fn placeholder_display_roundtrips() {
28491        // The Display impl must produce text that re-lexes to the
28492        // same Placeholder token.
28493        let s = parse("SELECT $42 FROM t");
28494        let printed = s.to_string();
28495        assert!(printed.contains("$42"));
28496        let again = parse(&printed);
28497        assert_eq!(s, again);
28498    }
28499
28500    #[test]
28501    fn alter_index_rebuild_bare() {
28502        use crate::ast::{AlterIndexTarget, Statement};
28503        let s = parse("ALTER INDEX my_idx REBUILD");
28504        let Statement::AlterIndex(a) = s else {
28505            panic!("expected AlterIndex, got {s:?}")
28506        };
28507        assert_eq!(a.name, "my_idx");
28508        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
28509    }
28510
28511    #[test]
28512    fn alter_index_rebuild_with_encoding() {
28513        use crate::ast::{AlterIndexTarget, Statement};
28514        for (sql, want) in [
28515            (
28516                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
28517                VecEncoding::F32,
28518            ),
28519            (
28520                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
28521                VecEncoding::Sq8,
28522            ),
28523            (
28524                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28525                VecEncoding::F16,
28526            ),
28527        ] {
28528            let s = parse(sql);
28529            let Statement::AlterIndex(a) = s else {
28530                panic!("{sql}: expected AlterIndex")
28531            };
28532            assert_eq!(a.name, "my_idx");
28533            assert_eq!(
28534                a.target,
28535                AlterIndexTarget::Rebuild {
28536                    encoding: Some(want)
28537                },
28538                "{sql}"
28539            );
28540        }
28541    }
28542
28543    #[test]
28544    fn alter_index_rebuild_unknown_encoding_errors() {
28545        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
28546        assert!(
28547            err.message.contains("unknown vector encoding"),
28548            "got: {}",
28549            err.message
28550        );
28551    }
28552
28553    #[test]
28554    fn alter_index_rebuild_display_roundtrips() {
28555        for (input, want) in [
28556            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
28557            (
28558                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28559                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28560            ),
28561            (
28562                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28563                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28564            ),
28565        ] {
28566            let s = parse(input);
28567            assert_eq!(s.to_string(), want);
28568        }
28569    }
28570
28571    #[test]
28572    fn create_table_unknown_type_defers_to_engine() {
28573        // v4.9 picked XML as a parse-time "unsupported column
28574        // type" probe. v7.17.0 Phase 1.4 changed the contract:
28575        // an unknown type ident parses as Text + `user_type_ref`
28576        // so CREATE TABLE can resolve user-defined enum / domain
28577        // types — rejection of truly-unknown types moved to the
28578        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
28579        // to a first-class built-in, so this probe switched to a
28580        // synthetic name nothing in the lexer will ever recognise.
28581        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
28582        let Statement::CreateTable(t) = stmt else {
28583            panic!("expected CreateTable");
28584        };
28585        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
28586    }
28587
28588    #[test]
28589    fn create_table_missing_table_keyword_errors() {
28590        assert!(parse_statement("CREATE x (a INT)").is_err());
28591    }
28592
28593    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
28594    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
28595
28596    #[test]
28597    fn parse_create_table_partition_by_range() {
28598        use crate::ast::{PartitionBySpec, PartitionKindAst};
28599        let stmt = parse_statement(
28600            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
28601             payload JSONB) PARTITION BY RANGE (ts)",
28602        )
28603        .unwrap();
28604        let Statement::CreateTable(t) = stmt else {
28605            panic!("expected CreateTable");
28606        };
28607        assert!(t.partition_of.is_none(), "parent has no partition_of");
28608        assert_eq!(t.columns.len(), 3);
28609        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
28610        assert_eq!(
28611            by,
28612            &PartitionBySpec {
28613                kind: PartitionKindAst::Range,
28614                key_columns: alloc::vec!["ts".to_string()],
28615            }
28616        );
28617        // Display round-trip preserves the suffix. `quote_ident`
28618        // only adds double quotes when the ident needs escaping, so
28619        // a plain `ts` survives bare here.
28620        assert!(
28621            t.to_string().contains("PARTITION BY RANGE (ts)"),
28622            "Display lost PARTITION BY suffix: {t}"
28623        );
28624    }
28625
28626    #[test]
28627    fn parse_create_table_partition_of_range() {
28628        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
28629        let stmt = parse_statement(
28630            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
28631             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
28632        )
28633        .unwrap();
28634        let Statement::CreateTable(t) = stmt else {
28635            panic!("expected CreateTable");
28636        };
28637        assert!(t.columns.is_empty(), "child inherits columns from parent");
28638        assert!(t.partition_by.is_none());
28639        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28640        assert_eq!(of.parent_name, "events_partitioned");
28641        let PartitionOfSpec { bounds, .. } = of.clone();
28642        match bounds {
28643            PartitionOfBoundsAst::Range { lower, upper } => {
28644                assert!(lower.to_string().contains("2026-06-01"));
28645                assert!(upper.to_string().contains("2026-07-01"));
28646            }
28647            other => panic!("expected Range, got {other:?}"),
28648        }
28649        // Display round-trip emits the FOR VALUES tail. `quote_ident`
28650        // skips quotes when not required, so the parent name appears
28651        // bare here.
28652        let s = t.to_string();
28653        assert!(
28654            s.contains("PARTITION OF events_partitioned"),
28655            "Display lost PARTITION OF: {s}"
28656        );
28657        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
28658        assert!(s.contains(") TO ("), "Display lost TO: {s}");
28659    }
28660
28661    #[test]
28662    fn parse_create_table_partition_of_default() {
28663        use crate::ast::PartitionOfBoundsAst;
28664        let stmt =
28665            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
28666                .unwrap();
28667        let Statement::CreateTable(t) = stmt else {
28668            panic!("expected CreateTable");
28669        };
28670        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28671        assert_eq!(of.parent_name, "events_partitioned");
28672        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
28673        assert!(
28674            t.to_string()
28675                .contains("PARTITION OF events_partitioned DEFAULT"),
28676            "Display lost DEFAULT: {t}"
28677        );
28678    }
28679
28680    #[test]
28681    fn parse_create_table_partition_by_list() {
28682        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
28683        // child with `FOR VALUES IN (lit, lit, …)`.
28684        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28685        let parent =
28686            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
28687                .unwrap();
28688        let Statement::CreateTable(t) = parent else {
28689            panic!("expected CreateTable");
28690        };
28691        let Some(PartitionBySpec {
28692            kind,
28693            ref key_columns,
28694        }) = t.partition_by
28695        else {
28696            panic!("expected PARTITION BY");
28697        };
28698        assert_eq!(kind, PartitionKindAst::List);
28699        assert_eq!(*key_columns, vec!["region".to_string()]);
28700        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
28701
28702        let child = parse_statement(
28703            "CREATE TABLE events_apac PARTITION OF events_listed \
28704             FOR VALUES IN ('jp', 'kr', 'tw')",
28705        )
28706        .unwrap();
28707        let Statement::CreateTable(c) = child else {
28708            panic!("expected CreateTable");
28709        };
28710        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28711        let PartitionOfBoundsAst::List { values } = &of.bounds else {
28712            panic!("expected List bounds, got {:?}", of.bounds);
28713        };
28714        assert_eq!(values.len(), 3);
28715        let disp = c.to_string();
28716        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
28717    }
28718
28719    #[test]
28720    fn parse_create_table_partition_by_hash() {
28721        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
28722        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
28723        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28724        let parent =
28725            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
28726        let Statement::CreateTable(t) = parent else {
28727            panic!("expected CreateTable");
28728        };
28729        let Some(PartitionBySpec {
28730            kind,
28731            ref key_columns,
28732        }) = t.partition_by
28733        else {
28734            panic!("expected PARTITION BY");
28735        };
28736        assert_eq!(kind, PartitionKindAst::Hash);
28737        assert_eq!(*key_columns, vec!["id".to_string()]);
28738        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
28739
28740        let child = parse_statement(
28741            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
28742             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
28743        )
28744        .unwrap();
28745        let Statement::CreateTable(c) = child else {
28746            panic!("expected CreateTable");
28747        };
28748        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28749        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
28750            panic!("expected Hash bounds");
28751        };
28752        assert_eq!(modulus, 4);
28753        assert_eq!(remainder, 0);
28754        let disp = c.to_string();
28755        assert!(
28756            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
28757            "Display lost HASH bounds: {disp}"
28758        );
28759
28760        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
28761        let bad = parse_statement(
28762            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
28763             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
28764        );
28765        let msg = format!("{}", bad.unwrap_err());
28766        assert!(
28767            msg.contains("REMAINDER") && msg.contains("MODULUS"),
28768            "expected REMAINDER/MODULUS validation error: {msg}"
28769        );
28770    }
28771
28772    #[test]
28773    fn parse_create_table_partition_of_rejects_columns() {
28774        // v7.37.6-B contract: PARTITION OF children inherit columns
28775        // from the parent; an explicit list MUST surface as a parse
28776        // error rather than getting silently ignored.
28777        let err = parse_statement(
28778            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
28779             FOR VALUES FROM ('a') TO ('b')",
28780        );
28781        assert!(err.is_err(), "expected parse error for explicit columns");
28782        let msg = format!("{}", err.unwrap_err());
28783        assert!(
28784            msg.contains("PARTITION OF") && msg.contains("column"),
28785            "error should mention PARTITION OF + columns: {msg}"
28786        );
28787    }
28788
28789    #[test]
28790    fn insert_single_value() {
28791        let s = parse("INSERT INTO foo VALUES (42)");
28792        let Statement::Insert(i) = s else {
28793            panic!("expected Insert")
28794        };
28795        assert_eq!(i.table, "foo");
28796        assert_eq!(i.rows.len(), 1);
28797        assert_eq!(i.rows[0].len(), 1);
28798        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
28799    }
28800
28801    #[test]
28802    fn insert_multi_value_with_mixed_literals() {
28803        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
28804        let Statement::Insert(i) = s else { panic!() };
28805        assert_eq!(i.rows.len(), 1);
28806        assert_eq!(i.rows[0].len(), 5);
28807    }
28808
28809    #[test]
28810    fn insert_missing_into_errors() {
28811        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
28812    }
28813
28814    #[test]
28815    fn create_table_round_trip() {
28816        let original =
28817            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
28818        let text = original.to_string();
28819        let again = parse_statement(&text).expect("re-parse");
28820        assert_eq!(original, again);
28821    }
28822
28823    #[test]
28824    fn insert_round_trip_with_negation_and_string() {
28825        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28826        let text = original.to_string();
28827        let again = parse_statement(&text).expect("re-parse");
28828        assert_eq!(original, again);
28829    }
28830
28831    #[test]
28832    fn unknown_keyword_at_statement_start_errors() {
28833        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28834        // the top-level dispatch still has no branch to take.
28835        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28836        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28837    }
28838
28839    // --- v0.8 CREATE INDEX --------------------------------------------------
28840
28841    #[test]
28842    fn create_index_basic() {
28843        let s = parse("CREATE INDEX idx_id ON users (id)");
28844        let Statement::CreateIndex(c) = s else {
28845            panic!("expected CreateIndex")
28846        };
28847        assert_eq!(c.name, "idx_id");
28848        assert_eq!(c.table, "users");
28849        assert_eq!(c.column, "id");
28850    }
28851
28852    #[test]
28853    fn create_index_missing_on_errors() {
28854        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28855    }
28856
28857    #[test]
28858    fn create_index_missing_paren_errors() {
28859        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28860    }
28861
28862    #[test]
28863    fn create_index_round_trip() {
28864        let original = parse("CREATE INDEX by_name ON users (name)");
28865        let again = parse_statement(&original.to_string()).unwrap();
28866        assert_eq!(original, again);
28867    }
28868
28869    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28870
28871    #[test]
28872    fn create_unique_index_basic() {
28873        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28874        let Statement::CreateIndex(c) = s else {
28875            panic!("expected CreateIndex");
28876        };
28877        assert!(c.is_unique);
28878        assert_eq!(c.column, "a");
28879        assert!(c.partial_predicate.is_none());
28880    }
28881
28882    #[test]
28883    fn create_unique_index_partial() {
28884        // mailrs's email_templates "one default per user" shape.
28885        let s = parse(
28886            "CREATE UNIQUE INDEX idx_email_templates_user_default \
28887             ON email_templates (user_address) WHERE is_default = true",
28888        );
28889        let Statement::CreateIndex(c) = s else {
28890            panic!("expected CreateIndex");
28891        };
28892        assert!(c.is_unique);
28893        assert_eq!(c.table, "email_templates");
28894        assert_eq!(c.column, "user_address");
28895        assert!(c.partial_predicate.is_some());
28896    }
28897
28898    #[test]
28899    fn create_unique_index_composite_with_predicate() {
28900        // mailrs's calendar_events instance: composite columns.
28901        let s = parse(
28902            "CREATE UNIQUE INDEX uq_calendar_events_instance \
28903             ON calendar_events (calendar_id, uid, recurrence_id) \
28904             WHERE recurrence_id IS NOT NULL",
28905        );
28906        let Statement::CreateIndex(c) = s else {
28907            panic!("expected CreateIndex");
28908        };
28909        assert!(c.is_unique);
28910        assert_eq!(c.column, "calendar_id");
28911        assert_eq!(
28912            c.extra_columns,
28913            vec!["uid".to_string(), "recurrence_id".to_string()]
28914        );
28915        assert!(c.partial_predicate.is_some());
28916    }
28917
28918    #[test]
28919    fn create_unique_index_using_btree_ok() {
28920        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28921        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28922    }
28923
28924    #[test]
28925    fn create_unique_index_using_hnsw_rejected() {
28926        let err =
28927            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28928        assert!(err.message.contains("UNIQUE"), "{}", err.message);
28929    }
28930
28931    #[test]
28932    fn create_unique_index_round_trip() {
28933        let original = parse(
28934            "CREATE UNIQUE INDEX uq_calendar_events_master \
28935             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28936        );
28937        let again = parse_statement(&original.to_string()).unwrap();
28938        assert_eq!(original, again);
28939    }
28940
28941    #[test]
28942    fn create_unique_without_index_errors() {
28943        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28944        // v7.39 (round 340, V56) — PG 18.4, verbatim.
28945        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28946    }
28947
28948    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28949
28950    #[test]
28951    fn create_table_bytea_column() {
28952        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28953        let Statement::CreateTable(c) = s else {
28954            panic!("expected CreateTable");
28955        };
28956        assert_eq!(c.columns.len(), 2);
28957        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28958        assert!(!c.columns[1].nullable);
28959    }
28960
28961    #[test]
28962    fn create_table_bytes_alias_column() {
28963        let s = parse("CREATE TABLE t (blob BYTES)");
28964        let Statement::CreateTable(c) = s else {
28965            panic!("expected CreateTable");
28966        };
28967        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28968    }
28969
28970    #[test]
28971    fn bytea_round_trip_display() {
28972        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28973        let again = parse_statement(&original.to_string()).unwrap();
28974        assert_eq!(original, again);
28975    }
28976
28977    // --- v0.9 transactions -------------------------------------------------
28978
28979    #[test]
28980    fn begin_commit_rollback_parse_as_unit_variants() {
28981        let plain = crate::ast::TransactionModes::default();
28982        assert_eq!(parse("BEGIN"), Statement::Begin(plain));
28983        assert_eq!(parse("COMMIT"), Statement::Commit);
28984        // r1066 — PG synonyms pgbench's tpcb script relies on.
28985        assert_eq!(parse("END"), Statement::Commit);
28986        assert_eq!(parse("END TRANSACTION"), Statement::Commit);
28987        assert_eq!(parse("COMMIT WORK"), Statement::Commit);
28988        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28989        // Trailing semicolons accepted too.
28990        assert_eq!(parse("BEGIN;"), Statement::Begin(plain));
28991        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28992        // statement (with or without the WORK/TRANSACTION noise word).
28993        assert_eq!(
28994            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28995            Statement::Begin(crate::ast::TransactionModes {
28996                isolation: Some(IsolationLevel::RepeatableRead),
28997                read_only: None,
28998            })
28999        );
29000        assert_eq!(
29001            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
29002            Statement::Begin(crate::ast::TransactionModes {
29003                isolation: Some(IsolationLevel::Serializable),
29004                read_only: None,
29005            })
29006        );
29007        // v7.39 — this line used to read
29008        //
29009        //     // A non-isolation mode keeps the session default (None).
29010        //     assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
29011        //
29012        // which pinned the defect rather than catching it: the READ ONLY
29013        // was thrown away, so the statement opened an ordinary read-write
29014        // transaction and every write inside it was accepted. The
29015        // isolation level is still absent here, because this statement
29016        // does not name one — that part was right.
29017        assert_eq!(
29018            parse("BEGIN READ ONLY"),
29019            Statement::Begin(crate::ast::TransactionModes {
29020                isolation: None,
29021                read_only: Some(true),
29022            })
29023        );
29024        assert_eq!(
29025            parse("START TRANSACTION READ WRITE"),
29026            Statement::Begin(crate::ast::TransactionModes {
29027                isolation: None,
29028                read_only: Some(false),
29029            })
29030        );
29031        assert_eq!(
29032            parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY"),
29033            Statement::Begin(crate::ast::TransactionModes {
29034                isolation: Some(IsolationLevel::Serializable),
29035                read_only: Some(true),
29036            })
29037        );
29038    }
29039
29040    // --- v1.2: pgvector distance ops + ::vector cast --------------------
29041
29042    #[test]
29043    fn inner_product_binop_parses() {
29044        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
29045        let Statement::Select(s) = s else { panic!() };
29046        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29047            panic!()
29048        };
29049        assert!(matches!(
29050            expr,
29051            Expr::Binary {
29052                op: BinOp::InnerProduct,
29053                ..
29054            }
29055        ));
29056    }
29057
29058    #[test]
29059    fn cosine_distance_binop_parses() {
29060        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
29061        let Statement::Select(s) = s else { panic!() };
29062        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29063            panic!()
29064        };
29065        assert!(matches!(
29066            expr,
29067            Expr::Binary {
29068                op: BinOp::CosineDistance,
29069                ..
29070            }
29071        ));
29072    }
29073
29074    #[test]
29075    fn vector_cast_postfix_wraps_string_literal() {
29076        let s = parse("SELECT '[1,2,3]'::vector FROM t");
29077        let Statement::Select(s) = s else { panic!() };
29078        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29079            panic!()
29080        };
29081        assert!(matches!(
29082            expr,
29083            Expr::Cast {
29084                target: CastTarget::Vector,
29085                ..
29086            }
29087        ));
29088    }
29089
29090    #[test]
29091    fn unsupported_cast_target_errors() {
29092        // v7.37.5 ship triage promoted the parser to accept every
29093        // ident as a `CastTarget::Named(canonical)`; the engine
29094        // surfaces the "unsupported cast target" error at eval
29095        // time when `type_name_to_data_type` can't resolve it.
29096        // Parser-side error now requires a NON-ident after `::`
29097        // (e.g. a punctuation token).
29098        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
29099        assert_eq!(err.message, "syntax error at or near \",\"");
29100    }
29101
29102    #[test]
29103    fn tx_statements_round_trip() {
29104        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
29105            let original = parse(q);
29106            let again = parse_statement(&original.to_string()).unwrap();
29107            assert_eq!(original, again);
29108        }
29109    }
29110
29111    #[test]
29112    fn interval_text_parsing_units() {
29113        // v7.37.5 β — three-field shape `(months, days, micros)` so
29114        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
29115        // Single unit.
29116        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
29117        assert_eq!(
29118            parse_interval_text("24 hours"),
29119            Some((0, 0, 86_400_000_000))
29120        );
29121        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
29122        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
29123        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
29124        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
29125        // Compound spans accumulate per-dimension.
29126        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
29127        assert_eq!(
29128            parse_interval_text("1 day 2 hours"),
29129            Some((0, 1, 7_200_000_000))
29130        );
29131        // Negative numbers carry through per-dimension.
29132        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
29133        // Bad shapes return None.
29134        assert_eq!(parse_interval_text(""), None);
29135        assert_eq!(parse_interval_text("garbage"), None);
29136        assert_eq!(parse_interval_text("1 fortnight"), None);
29137        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
29138        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
29139        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
29140        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
29141        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
29142    }
29143
29144    #[test]
29145    fn interval_literal_roundtrips_via_display() {
29146        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
29147        let s = parsed.to_string();
29148        // Display preserves the original text verbatim.
29149        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
29150        // And re-parsing yields a structurally equal statement.
29151        let again = parse_statement(&s).unwrap();
29152        assert_eq!(parsed, again);
29153    }
29154
29155    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
29156
29157    #[test]
29158    fn parser_recognises_create_publication_bare() {
29159        let s = parse("CREATE PUBLICATION pub_a");
29160        let Statement::CreatePublication(p) = s else {
29161            panic!("expected CreatePublication, got {s:?}")
29162        };
29163        assert_eq!(p.name, "pub_a");
29164        assert_eq!(p.scope, PublicationScope::AllTables);
29165    }
29166
29167    #[test]
29168    fn parser_recognises_create_publication_for_all_tables() {
29169        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
29170        let Statement::CreatePublication(p) = s else {
29171            panic!("expected CreatePublication, got {s:?}")
29172        };
29173        assert_eq!(p.name, "pub_a");
29174        assert_eq!(p.scope, PublicationScope::AllTables);
29175    }
29176
29177    #[test]
29178    fn parser_recognises_drop_publication() {
29179        let s = parse("DROP PUBLICATION pub_a");
29180        let Statement::DropPublication { name, .. } = s else {
29181            panic!("expected DropPublication, got {s:?}")
29182        };
29183        assert_eq!(name, "pub_a");
29184    }
29185
29186    #[test]
29187    fn parser_recognises_for_table_list() {
29188        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
29189        let Statement::CreatePublication(p) = s else {
29190            panic!("expected CreatePublication, got {s:?}")
29191        };
29192        assert_eq!(p.name, "pub_a");
29193        let PublicationScope::ForTables(ts) = p.scope else {
29194            panic!("expected ForTables scope")
29195        };
29196        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
29197    }
29198
29199    #[test]
29200    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
29201        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
29202        // is rejected (`invalid publication object list`; the old
29203        // test pinned an unverifiable "PG 19 accepts both" claim);
29204        // TABLES pairs with IN SCHEMA.
29205        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
29206            .expect_err("bare FOR TABLES must reject");
29207        assert!(
29208            alloc::format!("{err}").contains("invalid publication object list"),
29209            "got: {err}"
29210        );
29211        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
29212        let Statement::CreatePublication(p) = s else {
29213            panic!("expected CreatePublication, got {s:?}")
29214        };
29215        let PublicationScope::TablesInSchema(schema) = p.scope else {
29216            panic!("expected TablesInSchema")
29217        };
29218        assert_eq!(schema, "public");
29219    }
29220
29221    #[test]
29222    fn parser_recognises_for_all_tables_except_list() {
29223        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
29224        let Statement::CreatePublication(p) = s else {
29225            panic!()
29226        };
29227        let PublicationScope::AllTablesExcept(ts) = p.scope else {
29228            panic!("expected AllTablesExcept")
29229        };
29230        assert_eq!(ts, alloc::vec!["t1", "t2"]);
29231    }
29232
29233    #[test]
29234    fn parser_rejects_for_table_with_empty_list() {
29235        // `FOR TABLE` with nothing after is a parse error.
29236        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
29237            .expect_err("must error on empty list");
29238        // No specific message asserted — the call falls through to
29239        // expect_ident_like which yields "expected identifier, got …".
29240        assert!(!err.message.is_empty());
29241    }
29242
29243    #[test]
29244    fn parser_recognises_show_publications() {
29245        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
29246        // bare ident in this position, NOT a reserved keyword.
29247        let s = parse("SHOW PUBLICATIONS");
29248        assert!(matches!(s, Statement::ShowPublications));
29249    }
29250
29251    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
29252
29253    #[test]
29254    fn parser_recognises_create_subscription_single_publication() {
29255        let s = parse(
29256            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
29257        );
29258        let Statement::CreateSubscription(c) = s else {
29259            panic!("expected CreateSubscription, got {s:?}")
29260        };
29261        assert_eq!(c.name, "sub_a");
29262        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
29263        assert_eq!(c.publications, alloc::vec!["pub_a"]);
29264    }
29265
29266    #[test]
29267    fn parser_recognises_create_subscription_multi_publication() {
29268        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
29269        let Statement::CreateSubscription(c) = s else {
29270            panic!()
29271        };
29272        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
29273    }
29274
29275    #[test]
29276    fn parser_rejects_create_subscription_missing_connection() {
29277        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
29278            .expect_err("must error on missing CONNECTION");
29279        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
29280    }
29281
29282    #[test]
29283    fn parser_rejects_create_subscription_missing_publication() {
29284        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
29285            .expect_err("must error on missing PUBLICATION");
29286        assert_eq!(err.message, "syntax error at end of input");
29287    }
29288
29289    #[test]
29290    fn parser_recognises_drop_subscription() {
29291        let s = parse("DROP SUBSCRIPTION sub_a");
29292        let Statement::DropSubscription { name, .. } = s else {
29293            panic!("expected DropSubscription, got {s:?}")
29294        };
29295        assert_eq!(name, "sub_a");
29296    }
29297
29298    #[test]
29299    fn parser_recognises_show_subscriptions() {
29300        let s = parse("SHOW SUBSCRIPTIONS");
29301        assert!(matches!(s, Statement::ShowSubscriptions));
29302    }
29303
29304    #[test]
29305    fn parser_recognises_wait_for_wal_position_no_timeout() {
29306        let s = parse("WAIT FOR WAL POSITION 12345");
29307        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29308            panic!("expected WaitForWalPosition, got {s:?}")
29309        };
29310        assert_eq!(pos, 12345);
29311        assert!(timeout_ms.is_none());
29312    }
29313
29314    #[test]
29315    fn parser_recognises_wait_for_wal_position_with_timeout() {
29316        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
29317        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29318            panic!()
29319        };
29320        assert_eq!(pos, 67890);
29321        assert_eq!(timeout_ms, Some(5000));
29322    }
29323
29324    #[test]
29325    fn parser_rejects_wait_with_negative_position() {
29326        // The lexer treats `-` as a token; `expect_u64_literal`
29327        // only sees the Integer that follows, so the negative
29328        // arrives as a unary-minus expression at higher levels.
29329        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
29330        // parse error one way or another.
29331        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
29332        assert!(!err.message.is_empty());
29333    }
29334
29335    #[test]
29336    fn parser_recognises_bare_analyze() {
29337        let s = parse("ANALYZE");
29338        assert!(matches!(s, Statement::Analyze(None)));
29339    }
29340
29341    #[test]
29342    fn parser_recognises_analyze_with_table() {
29343        let s = parse("ANALYZE users");
29344        let Statement::Analyze(Some(name)) = s else {
29345            panic!("expected Analyze, got {s:?}")
29346        };
29347        assert_eq!(name, "users");
29348    }
29349
29350    #[test]
29351    fn parser_recognises_analyze_with_quoted_table() {
29352        let s = parse("ANALYZE \"Mixed Case\"");
29353        let Statement::Analyze(Some(name)) = s else {
29354            panic!()
29355        };
29356        assert_eq!(name, "Mixed Case");
29357    }
29358
29359    #[test]
29360    fn parser_rejects_analyze_with_garbage_token() {
29361        let err = parse_statement("ANALYZE 42").expect_err("must error");
29362        assert!(!err.message.is_empty());
29363    }
29364
29365    #[test]
29366    fn analyze_display_roundtrips() {
29367        for sql in ["ANALYZE", "ANALYZE users"] {
29368            let s = parse(sql);
29369            let printed = s.to_string();
29370            let again = parse_statement(&printed)
29371                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29372            assert_eq!(s, again);
29373        }
29374    }
29375
29376    #[test]
29377    fn wait_for_display_roundtrips() {
29378        for sql in [
29379            "WAIT FOR WAL POSITION 12345",
29380            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
29381        ] {
29382            let s = parse(sql);
29383            let printed = s.to_string();
29384            let again = parse_statement(&printed)
29385                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29386            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29387        }
29388    }
29389
29390    #[test]
29391    fn subscription_ddl_display_roundtrips() {
29392        for sql in [
29393            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
29394            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
29395            "DROP SUBSCRIPTION sub_a",
29396            "SHOW SUBSCRIPTIONS",
29397        ] {
29398            let s = parse(sql);
29399            let printed = s.to_string();
29400            let again = parse_statement(&printed)
29401                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29402            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29403        }
29404    }
29405
29406    #[test]
29407    fn parser_drop_dispatches_user_vs_publication() {
29408        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
29409        // tokenises DROP. Both targets must still parse.
29410        let s = parse("DROP USER 'alice'");
29411        let Statement::DropUser { name, .. } = s else {
29412            panic!("expected DropUser, got {s:?}")
29413        };
29414        assert_eq!(name, "alice");
29415        // And DROP PUBLICATION lands the new variant.
29416        let s = parse("DROP PUBLICATION p1");
29417        assert!(matches!(s, Statement::DropPublication { .. }));
29418    }
29419
29420    #[test]
29421    fn publication_ddl_display_roundtrips() {
29422        // Every CREATE PUBLICATION variant must Display → parse →
29423        // same AST. v6.1.3 covers all three scope shapes.
29424        for sql in [
29425            "CREATE PUBLICATION pub_a",
29426            "CREATE PUBLICATION pub_a FOR ALL TABLES",
29427            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
29428            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
29429            "DROP PUBLICATION pub_a",
29430            "SHOW PUBLICATIONS",
29431        ] {
29432            let s = parse(sql);
29433            let printed = s.to_string();
29434            let again = parse_statement(&printed)
29435                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29436            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29437        }
29438    }
29439
29440    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
29441
29442    #[test]
29443    fn create_function_returns_trigger_plpgsql_minimal() {
29444        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
29445        let s = parse(sql);
29446        let Statement::CreateFunction(f) = s else {
29447            panic!("expected CreateFunction");
29448        };
29449        assert_eq!(f.name, "noop");
29450        assert!(!f.or_replace);
29451        assert!(f.args.is_empty());
29452        assert!(matches!(f.returns, FunctionReturn::Trigger));
29453        assert_eq!(f.language, "plpgsql");
29454        let FunctionBody::PlPgSql(block) = f.body else {
29455            panic!("expected PlPgSql body");
29456        };
29457        assert_eq!(block.statements.len(), 1);
29458        assert!(matches!(
29459            block.statements[0],
29460            PlPgSqlStmt::Return(ReturnTarget::New)
29461        ));
29462    }
29463
29464    #[test]
29465    fn create_function_or_replace_with_assignment() {
29466        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
29467        // RETURN NEW.
29468        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
29469BEGIN
29470  NEW.search_vector := to_tsvector('english', NEW.subject);
29471  RETURN NEW;
29472END;
29473$$";
29474        let s = parse(sql);
29475        let Statement::CreateFunction(f) = s else {
29476            panic!("expected CreateFunction");
29477        };
29478        assert!(f.or_replace);
29479        let FunctionBody::PlPgSql(block) = &f.body else {
29480            panic!("expected PlPgSql body");
29481        };
29482        assert_eq!(block.statements.len(), 2);
29483        // First statement: NEW.search_vector := to_tsvector(...)
29484        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
29485            panic!("expected Assign as first stmt");
29486        };
29487        match target {
29488            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
29489            other => panic!("expected NEW.col, got {other:?}"),
29490        }
29491        // Second statement: RETURN NEW
29492        assert!(matches!(
29493            block.statements[1],
29494            PlPgSqlStmt::Return(ReturnTarget::New)
29495        ));
29496    }
29497
29498    #[test]
29499    fn create_trigger_after_insert_or_update() {
29500        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
29501        let s = parse(sql);
29502        let Statement::CreateTrigger(t) = s else {
29503            panic!("expected CreateTrigger");
29504        };
29505        assert_eq!(t.name, "tg");
29506        assert_eq!(t.table, "messages");
29507        assert_eq!(t.timing, TriggerTiming::After);
29508        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
29509        assert_eq!(t.for_each, TriggerForEach::Row);
29510        assert_eq!(t.function, "update_sv");
29511    }
29512
29513    #[test]
29514    fn create_trigger_before_delete_execute_procedure_alias() {
29515        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
29516        let sql =
29517            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
29518        let s = parse(sql);
29519        let Statement::CreateTrigger(t) = s else {
29520            panic!("expected CreateTrigger");
29521        };
29522        assert_eq!(t.timing, TriggerTiming::Before);
29523        assert_eq!(t.events, vec![TriggerEvent::Delete]);
29524    }
29525
29526    #[test]
29527    fn drop_trigger_if_exists_round_trips() {
29528        // No parser support for DROP TRIGGER yet — added in v7.12.5
29529        // alongside the broader DROP …{IF EXISTS} cleanup. The
29530        // AST + Display impls are in place so we round-trip via
29531        // construction:
29532        let s = Statement::DropTrigger {
29533            name: "tg".into(),
29534            table: "messages".into(),
29535            if_exists: true,
29536        };
29537        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
29538    }
29539
29540    #[test]
29541    fn trigger_ddl_display_roundtrips_through_parser() {
29542        // CREATE TRIGGER + its referenced CREATE FUNCTION must
29543        // Display → parse → same AST (modulo PL/pgSQL body
29544        // formatting which is parser-canonicalised).
29545        for sql in [
29546            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
29547            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
29548        ] {
29549            let s = parse(sql);
29550            let printed = s.to_string();
29551            let again = parse_statement(&printed)
29552                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29553            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29554        }
29555    }
29556}