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            | "use"
73            // PG psql backslash meta-commands that newer
74            // pg_dump versions emit unescaped (\restrict /
75            // \unrestrict). Real psql intercepts these; SPG's
76            // PG-wire sees them as raw text.
77            | "\\restrict"
78            | "\\unrestrict"
79            // v7.17.0 Phase 4.1 — MySQL `DELIMITER //` and
80            // `DELIMITER ;` directives. Technically client-side
81            // (the `mysql` CLI uses them to set the statement
82            // terminator), not SQL — but mysqldump and stored-
83            // procedure scripts emit them inline. SPG's parser
84            // sees one statement at a time and doesn't care
85            // about the terminator, so consume DELIMITER lines
86            // as Empty.
87            | "delimiter"
88            // v7.37.17 (17.6 siblings) — additional PG maintenance /
89            // session-state statements pg_dump + application startup
90            // scripts emit. SPG has no matching session-state to
91            // discard (no prepared-plan cache surface, no temp
92            // sequences), no matching security-label / storage-
93            // option to apply, no separate CREATE/DROP CAST that
94            // affects execution.
95            // v7.37.17 (17.6 siblings) — PG role-cleanup statements
96            // pg_dump / pg_dumpall emit around DROP ROLE:
97            //   REASSIGN OWNED BY <role> [, ...] TO <newrole>
98            //   DROP OWNED BY <role> [, ...] [CASCADE | RESTRICT]
99            // Both operate on the role's owned objects; SPG has no
100            // role-owner model, so accept-and-no-op.
101            // v7.37.17 (17.6 sibling) — LOAD '<library>'. pg_dump
102            // + extension scripts use LOAD to preload shared
103            // libraries. SPG doesn't have a shared-library extension
104            // point today (extensions ship as first-class crates
105            // linked at build time); accept as a no-op.
106            | "load"
107    )
108}
109
110/// v7.37.43-T4 — PG-unreserved keywords that are legal identifiers
111/// per `pg_get_keywords()`. SPG tokenizes these as named variants
112/// so the parser can dispatch on them in their owning contexts
113/// (`RELEASE SAVEPOINT`, `SHOW name`, `BEGIN`/`COMMIT`/`ROLLBACK`,
114/// `CREATE INDEX`, etc.), but they MUST stay usable as table /
115/// column / alias names — that's the PG contract for unreserved
116/// keywords (see PG docs Appendix C.1).
117///
118/// Before this generalisation, sentori migration 0001_init.sql
119/// `release TEXT NOT NULL` blew up the parser with "expected
120/// identifier, got Release", and the same gap stalked every
121/// SPG drop-in user whose schema had a column / alias named
122/// `release` / `index` / `tables` / `show` / `savepoint` /
123/// `begin` / `commit` / `rollback` / `drop` / `insert` / `values`
124/// / `limit` / `partition`. PG accepts all of them as identifiers
125/// when unquoted, so SPG must too.
126///
127/// Returns the canonical lowercase identifier text when the token
128/// belongs to PG's unreserved class, `None` otherwise. Used by
129/// `expect_ident_like` (column / table / alias names) so the
130/// generalisation applies everywhere an identifier may appear,
131/// not just in the contexts these tokens were introduced for.
132fn unreserved_keyword_text(tok: &Token) -> Option<String> {
133    let s = match tok {
134        // PG keyword class: unreserved or col_name.
135        //
136        Token::Release => "release",
137        Token::Savepoint => "savepoint",
138        Token::Show => "show",
139        Token::Index => "index",
140        Token::Begin => "begin",
141        Token::Commit => "commit",
142        Token::Rollback => "rollback",
143        Token::Drop => "drop",
144        Token::Insert => "insert",
145        Token::Values => "values",
146        Token::Limit => "limit",
147        Token::Partition => "partition",
148        Token::Tables => "tables",
149        Token::Connection => "connection",
150        Token::Publication => "publication",
151        Token::Subscription => "subscription",
152        Token::Interval => "interval",
153        // `extract` is non-reserved in PG too (it's a function the
154        // parser dispatches via context — outside that context it's
155        // a plain identifier).
156        Token::Extract => "extract",
157        Token::Offset => "offset",
158        // `to` is reserved in PG (used in many "AS … TO …" forms), so
159        // it is NOT relaxed here. Same for `from`, `where`, `as`,
160        // `select`, `not`, `and`, `or`, `null`, `true`, `false`,
161        // `create`, `table`, `into`, `on`, `order`, `by`, `having`,
162        // `group`, `distinct`, `union`, `all`, `join`, `inner`,
163        // `left`, `cross`, `outer`, `default`, `is`, `between`,
164        // `in`, `like`, `for`, `except`, `desc`, `asc`, `partition`
165        // (partial — keep partition as unreserved per modern PG).
166        _ => return None,
167    };
168    Some(s.to_string())
169}
170
171/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
172/// in CREATE INDEX. SPG's HNSW already routes by query operator;
173/// the opclass is accepted for `pg_dump` compatibility (mailrs
174/// migration follow-up G5).
175/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
176/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
177/// doesn't change index behaviour based on them.
178/// v7.37.17 (17.6 siblings) — the four PG `each` SRFs share one
179/// FROM-clause pipeline; the stored name tells the executor whether
180/// the value column keeps JSON rendering (`jsonb_each` / `json_each`)
181/// or unwraps to text (`*_each_text`).
182fn is_json_each_name(s: &str) -> bool {
183    s.eq_ignore_ascii_case("jsonb_each_text")
184        || s.eq_ignore_ascii_case("jsonb_each")
185        || s.eq_ignore_ascii_case("json_each_text")
186        || s.eq_ignore_ascii_case("json_each")
187}
188
189/// v7.38 (read01, T14) — resolve named function arguments (`argname => value`)
190/// to positional order for the `make_*` family (the AST stays positional).
191/// Positional args fill slots left-to-right; a named arg goes to its registered
192/// slot; unfilled slots default to integer 0 (PG's optional make_interval
193/// fields — the make_date/time arity is still checked at eval time).
194fn reorder_named_args(
195    fname: &str,
196    args: Vec<Expr>,
197    names: &[Option<String>],
198) -> Result<Vec<Expr>, String> {
199    let params: &[&str] = match fname.to_ascii_lowercase().as_str() {
200        "make_date" => &["year", "month", "day"],
201        "make_time" => &["hour", "min", "sec"],
202        "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
203        "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
204        other => {
205            return Err(alloc::format!(
206                "function {other}(...) does not support named arguments"
207            ));
208        }
209    };
210    let mut slots: Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
211    let mut next_positional = 0usize;
212    for (arg, name) in args.into_iter().zip(names.iter()) {
213        let idx = match name {
214            Some(n) => params
215                .iter()
216                .position(|p| p.eq_ignore_ascii_case(n))
217                .ok_or_else(|| alloc::format!("{fname}(...) has no argument named \"{n}\""))?,
218            None => {
219                let i = next_positional;
220                next_positional += 1;
221                i
222            }
223        };
224        if idx >= slots.len() {
225            return Err(alloc::format!("too many arguments for {fname}(...)"));
226        }
227        if slots[idx].is_some() {
228            return Err(alloc::format!(
229                "argument \"{}\" specified more than once",
230                params[idx]
231            ));
232        }
233        slots[idx] = Some(arg);
234    }
235    Ok(slots
236        .into_iter()
237        .map(|s| s.unwrap_or(Expr::Literal(Literal::Integer(0))))
238        .collect())
239}
240
241/// v7.38 (read01) — parse a lexer `Token::Numeric` source string (digits with
242/// an optional single `.`, no sign, no exponent) into `(unscaled, scale)` for
243/// `Literal::Numeric`. Returns `None` if the mantissa overflows i128.
244/// v7.39 (read01 numeric.c) — the result of expanding an `1.5e3`-style
245/// scientific literal into PG's plain NUMERIC decimal form.
246#[derive(Debug)]
247pub enum SciExpanded {
248    /// Plain decimal string ("1.5e3" → "1500", "1e-5" → "0.00001").
249    Expanded(String),
250    /// Exponent pushes the value outside PG's numeric format
251    /// (more than 131072 integer digits or 16383 fractional digits).
252    Overflow,
253    /// Not a `[±]digits[.digits]e[±]digits` literal at all.
254    NotScientific,
255}
256
257/// Expand scientific notation into a plain decimal string by moving the
258/// decimal point — no float round-trip, so the value stays exact. PG treats
259/// such literals as NUMERIC; the digit-count caps mirror PG's numeric format
260/// limits ("value overflows numeric format").
261pub fn expand_scientific_literal(s: &str) -> SciExpanded {
262    let s = s.trim();
263    let Some(epos) = s.find(['e', 'E']) else {
264        return SciExpanded::NotScientific;
265    };
266    let (mant, exp_str) = (&s[..epos], &s[epos + 1..]);
267    let Ok(exp) = exp_str.parse::<i64>() else {
268        return SciExpanded::NotScientific;
269    };
270    let (neg, mant) = match mant.strip_prefix('-') {
271        Some(r) => (true, r),
272        None => (false, mant.strip_prefix('+').unwrap_or(mant)),
273    };
274    let (int_part, frac_part) = match mant.split_once('.') {
275        Some((i, f)) => (i, f),
276        None => (mant, ""),
277    };
278    if (int_part.is_empty() && frac_part.is_empty())
279        || !int_part.bytes().all(|b| b.is_ascii_digit())
280        || !frac_part.bytes().all(|b| b.is_ascii_digit())
281    {
282        return SciExpanded::NotScientific;
283    }
284    let mut digits = String::with_capacity(int_part.len() + frac_part.len());
285    digits.push_str(int_part);
286    digits.push_str(frac_part);
287    // Decimal point position within `digits` after applying the exponent.
288    let new_point = int_part.len() as i64 + exp;
289    // PG's numeric format: up to 131072 digits before the point, 16383 after.
290    if new_point > 131_072 {
291        return SciExpanded::Overflow;
292    }
293    if (digits.len() as i64 - new_point) > 16_383 {
294        return SciExpanded::Overflow;
295    }
296    let sign = if neg { "-" } else { "" };
297    let plain = if new_point <= 0 {
298        let mut out = String::with_capacity(digits.len() + 2 + (-new_point) as usize);
299        out.push_str("0.");
300        for _ in 0..(-new_point) {
301            out.push('0');
302        }
303        out.push_str(&digits);
304        out
305    } else if (new_point as usize) >= digits.len() {
306        let mut out = digits;
307        for _ in 0..(new_point as usize - out.len()) {
308            out.push('0');
309        }
310        out
311    } else {
312        let mut out = String::with_capacity(digits.len() + 1);
313        out.push_str(&digits[..new_point as usize]);
314        out.push('.');
315        out.push_str(&digits[new_point as usize..]);
316        out
317    };
318    SciExpanded::Expanded(alloc::format!("{sign}{plain}"))
319}
320
321/// v7.39 (round 367, M20) — lower a MySQL hexadecimal binary-string
322/// literal (`0x…` / `X'…'`) onto the existing bytea cast. The hex digits
323/// are left-padded to an even count (`0x123` → byte string `01 23`, per
324/// MariaDB) and handed to the PG bytea input format (`\x…`).
325#[inline(never)]
326fn hex_literal_to_bytea_expr(hex: &str) -> Expr {
327    let padded = if hex.len() % 2 == 1 {
328        alloc::format!("0{hex}")
329    } else {
330        hex.to_string()
331    };
332    Expr::Cast {
333        expr: alloc::boxed::Box::new(Expr::Literal(Literal::String(alloc::format!(
334            "\\x{padded}"
335        )))),
336        target: CastTarget::Named("bytea".to_string()),
337    }
338}
339
340/// v7.39 (round 367, M20) — lower a MySQL bit-value literal (`b'1010'`)
341/// onto the bytea cast. The bits are read big-endian and left-padded to a
342/// whole number of bytes (`b'1010'` → one byte `0x0A`, per MariaDB).
343#[inline(never)]
344fn bits_literal_to_bytea_expr(bits: &str) -> Expr {
345    let pad = (8 - bits.len() % 8) % 8;
346    let mut hex = String::with_capacity((bits.len() + pad).div_ceil(4));
347    let padded: String = core::iter::repeat_n('0', pad).chain(bits.chars()).collect();
348    for nibble in padded.as_bytes().chunks(4) {
349        let mut v = 0u8;
350        for &b in nibble {
351            v = (v << 1) | (b - b'0');
352        }
353        hex.push(char::from_digit(u32::from(v), 16).unwrap_or('0'));
354    }
355    hex_literal_to_bytea_expr(&hex)
356}
357
358/// Resolve a lexer `Token::Numeric` into its literal. PG semantics: a dotted
359/// or over-i64 literal is exact NUMERIC; scientific notation is NUMERIC too
360/// (expanded to the plain decimal form); only a fractional depth beyond SPG's
361/// scale width (u8) falls back to double precision.
362///
363/// v7.38.19 — that sentence used to end "(recorded delta)". Measured
364/// against PG 18.4: a literal with 300 fractional digits round-trips
365/// identically on both engines, so whatever the note described is gone.
366/// It is RD-7 in `docs/RECORDED_DELTAS.md`, under "corrected by
367/// measurement" rather than under "open".
368/// Kept out of the parse_expr recursion frame — see the call site.
369#[inline(never)]
370fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
371    match parse_decimal_literal(&s) {
372        Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
373        // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
374        // its exact value as a NumericBig.
375        None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
376        // v7.39 (read01 numeric.c) — expand the exponent form.
377        None => match expand_scientific_literal(&s) {
378            SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
379                Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
380                None if plain
381                    .split_once('.')
382                    .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
383                {
384                    Ok(Literal::NumericBig(plain))
385                }
386                None => s
387                    .parse::<f64>()
388                    .map(Literal::Float)
389                    .map_err(|_| format!("invalid numeric literal {s:?}")),
390            },
391            SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
392            SciExpanded::NotScientific => s
393                .parse::<f64>()
394                .map(Literal::Float)
395                .map_err(|_| format!("invalid numeric literal {s:?}")),
396        },
397    }
398}
399
400fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
401    let (int_part, frac_part) = match s.split_once('.') {
402        Some((i, f)) => (i, f),
403        None => (s, ""),
404    };
405    // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
406    // places fell out of the numeric path here, which is why
407    // `pg_typeof(1e-256)` answered double precision and a plain
408    // 256-place decimal aborted the query in the big-decimal converter.
409    if frac_part.len() > u16::MAX as usize {
410        return None;
411    }
412    let mut digits = String::with_capacity(int_part.len() + frac_part.len());
413    digits.push_str(int_part);
414    digits.push_str(frac_part);
415    let mantissa: i128 = digits.parse().ok()?;
416    #[allow(clippy::cast_possible_truncation)]
417    Some((mantissa, frac_part.len() as u16))
418}
419
420/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
421/// record-returning JSON functions that take a `AS alias(col type, …)`
422/// column-definition list in FROM position.
423fn is_json_to_record_name(s: &str) -> bool {
424    s.eq_ignore_ascii_case("jsonb_to_recordset")
425        || s.eq_ignore_ascii_case("jsonb_to_record")
426        // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
427        // column-definition list desugars identically (the record base
428        // argument only carries the type; a non-NULL base's field
429        // defaults are a recorded delta, RD-6).
430        || s.eq_ignore_ascii_case("json_populate_record")
431        || s.eq_ignore_ascii_case("jsonb_populate_record")
432        || s.eq_ignore_ascii_case("json_populate_recordset")
433        || s.eq_ignore_ascii_case("jsonb_populate_recordset")
434        || s.eq_ignore_ascii_case("json_to_recordset")
435        || s.eq_ignore_ascii_case("json_to_record")
436}
437
438impl Parser {
439    /// Whether what follows an identifier ends an index key, which is how
440    /// an operator class is told from anything else in that position.
441    fn opclass_position_follows(next: Option<&Token>) -> bool {
442        match next {
443            // `ASC` / `DESC` have their own tokens; matching them as
444            // identifiers named "asc" / "desc" — which the first version of
445            // this did — never fires, and `(c text_pattern_ops DESC)` (which
446            // PG18.4 accepts, verified) went on failing to parse.
447            Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
448            Some(Token::Ident(w)) => {
449                w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
450            }
451            _ => false,
452        }
453    }
454}
455
456fn is_vector_opclass_name(name: &str) -> bool {
457    let lc = name.to_ascii_lowercase();
458    matches!(
459        lc.as_str(),
460        "vector_cosine_ops"
461            | "vector_l2_ops"
462            | "vector_ip_ops"
463            | "halfvec_cosine_ops"
464            | "halfvec_l2_ops"
465            | "halfvec_ip_ops"
466            | "sq8_cosine_ops"
467            | "sq8_l2_ops"
468            | "sq8_ip_ops"
469            // pg_trgm — trigram operator class. SPG's GIN index
470            // already uses tsvector tokens; trigram-style LIKE
471            // pattern matching still routes through a sequential
472            // scan, but the opclass name is accepted so PG schemas
473            // load.
474            | "gin_trgm_ops"
475            | "gist_trgm_ops"
476            // PG built-in btree opclasses occasionally appear in
477            // pg_dump output for column types with multiple
478            // sort orders (text_pattern_ops, varchar_pattern_ops,
479            // bpchar_pattern_ops).
480            | "text_pattern_ops"
481            | "varchar_pattern_ops"
482            | "bpchar_pattern_ops"
483            | "int4_ops"
484            | "int8_ops"
485            | "text_ops"
486    )
487}
488
489#[derive(Debug, Clone, PartialEq, Eq)]
490pub struct ParseError {
491    pub message: String,
492    /// Index into the token stream where parsing tripped. Not a byte offset.
493    /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
494    /// field would grow every `Result<_, ParseError>` slot on the deeply
495    /// recursive parse stack and tip the nesting-budget frame cliff. PG's
496    /// 1-based char position is recovered on the cold error path by
497    /// [`syntax_error_position`], which re-tokenizes to map this token index.
498    pub token_pos: usize,
499}
500
501impl fmt::Display for ParseError {
502    /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
503    /// with `parse error at token #N: `, which PG has no equivalent of:
504    /// the message bodies are already PG's verbatim (`LIMIT must not be
505    /// negative`, `invalid input syntax for type bigint: "abc"`), and the
506    /// prefix was SPG's internal token index leaking into every one of
507    /// them. `token_pos` stays a field — the wire recovers PG's 1-based
508    /// character position from it for the ErrorResponse `P`.
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510        f.write_str(&self.message)
511    }
512}
513
514impl From<LexError> for ParseError {
515    fn from(e: LexError) -> Self {
516        Self {
517            message: format!("lex: {e}"),
518            token_pos: 0,
519        }
520    }
521}
522
523/// v7.9.30 — parse a single expression (no trailing junk). Used by
524/// the engine to re-hydrate stored partial-index / unique-index
525/// predicates from their canonical Display form. The same Pratt
526/// parser the statement path uses; this entry point just skips the
527/// statement dispatch.
528pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
529    let (tokens, offsets) =
530        lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
531    let mut p = Parser::new(tokens);
532    let expr = p
533        .parse_expr(0)
534        .and_then(|e| p.expect_eof().map(|()| e))
535        .map_err(|e| shape_syntax_error(e, input, &offsets))?;
536    Ok(expr)
537}
538
539/// Parse exactly one statement, swallow an optional trailing `;`, and require
540/// the token stream to end there. PG string semantics.
541pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
542    parse_statement_with(input, false)
543}
544
545/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
546/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
547/// The engine threads its session flag through here.
548pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
549    let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
550        .map_err(|e| shape_lex_error(&e, input))?;
551    // The same session flag names the dialect for both the lexer and
552    // the type mapping.
553    let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
554    let stmt = (|| {
555        let stmt = p.parse_one_statement()?;
556        if matches!(p.peek(), Token::Semicolon) {
557            p.advance();
558        }
559        p.expect_eof()?;
560        Ok(stmt)
561    })()
562    .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
563    Ok(stmt)
564}
565
566/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
567/// `syntax error at or near "<token>"` and `syntax error at end of input`
568/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
569/// prose — `expected identifier, got Eof`, `unexpected token From in
570/// expression`, `expected end of input, got Ident("with")` — which named
571/// internal token types and, in the Debug forms, leaked the parser's own
572/// enum into a message clients read.
573///
574/// Applied once on the way out, so every construction site is covered and
575/// the token named is the one the error itself points at. Messages whose
576/// bodies are already PG's verbatim (`LIMIT must not be negative`,
577/// `invalid input syntax for type bigint: "abc"`) are left alone — those
578/// are PG's own errors, not its syntax error.
579fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
580    if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
581        return e;
582    }
583    let message = match offending_lexeme(input, offsets, e.token_pos) {
584        Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
585        None => "syntax error at end of input".into(),
586    };
587    ParseError {
588        message,
589        token_pos: e.token_pos,
590    }
591}
592
593/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
594/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
595/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
596/// comment at or near "/* x"` — the quoted part runs from the opening
597/// delimiter to the end of the input. SPG reported its own internal
598/// shape instead (`unterminated string literal at byte 7`), which named
599/// a byte offset no client can use.
600fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
601    use lexer::LexErrorKind as K;
602    let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
603    let message = match &e.kind {
604        K::UnterminatedString => {
605            alloc::format!("unterminated quoted string at or near \"{from_here}\"")
606        }
607        K::UnterminatedQuotedIdent => {
608            alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
609        }
610        K::UnterminatedBlockComment => {
611            alloc::format!("unterminated /* comment at or near \"{from_here}\"")
612        }
613        // PG has no "unknown character" error of its own — the character
614        // is skipped and the parser reports the next token. SPG stops at
615        // the character itself and names it, which is the same shape.
616        K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
617        // The number-literal kinds already carry PG's `at or near` form.
618        other => alloc::format!(
619            "{}",
620            lexer::LexError {
621                kind: other.clone(),
622                pos: e.pos,
623            }
624        ),
625    };
626    ParseError {
627        message,
628        token_pos: 0,
629    }
630}
631
632/// The offending token exactly as it appears in the input, or `None` at
633/// end of input. PG echoes the source spelling — a lower-case `frm`
634/// reports as `frm`, not as a canonicalised keyword.
635fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
636    let start = *offsets.get(token_pos)?;
637    if start >= input.len() {
638        return None;
639    }
640    let end = offsets
641        .get(token_pos + 1)
642        .copied()
643        .unwrap_or(input.len())
644        .min(input.len());
645    let seg = input.get(start..end)?.trim();
646    if seg.is_empty() {
647        return None;
648    }
649    // A quoted literal / identifier keeps its inner spaces; anything else
650    // ends at the first whitespace (the segment runs to the NEXT token's
651    // start, which may swallow a comment).
652    if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
653        Some(seg)
654    } else {
655        seg.split_whitespace().next()
656    }
657}
658
659/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
660/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
661/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
662/// this re-tokenizes `input` on the cold error path to map the failing token
663/// index to its start byte, then to a character offset. `backslash_escapes`
664/// must match the parse that produced `token_pos` (it barely shifts offsets,
665/// but stay consistent). Returns `None` when the index has no offset or the
666/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
667#[must_use]
668pub fn syntax_error_position(
669    input: &str,
670    backslash_escapes: bool,
671    token_pos: usize,
672) -> Option<usize> {
673    let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
674    let byte_off = *offsets.get(token_pos)?;
675    if byte_off > input.len() || !input.is_char_boundary(byte_off) {
676        return None;
677    }
678    Some(input[..byte_off].chars().count() + 1)
679}
680
681struct Parser {
682    tokens: Vec<Token>,
683    pos: usize,
684    /// v7.39 (round 274) — the session's dialect, carried by the same
685    /// signal that drives string-literal escaping: `SET sql_mode` (only
686    /// MySQL clients and mysqldump preambles emit it) turns it on,
687    /// `SET standard_conforming_strings` (every pg_dump preamble) turns
688    /// it off. Needed here because the two dialects disagree about what
689    /// `REAL` means — see the type mapping below.
690    mysql_dialect: bool,
691    /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
692    /// mutually recursive expr/select parsers. Bounded so a deeply
693    /// nested input returns a parse error instead of overflowing
694    /// the stack (embed hosts die on overflow — it is an abort,
695    /// not a catchable error).
696    nest_depth: usize,
697    /// TABLESAMPLE lowering channel: the table-ref parser pushes a
698    /// `random() < p/100` predicate here; the enclosing SELECT
699    /// drains the list after its WHERE parses and ANDs the
700    /// predicates in. parse_bare_select save/restores around its
701    /// FROM+WHERE so nested selects only drain their own.
702    pending_sample_preds: Vec<Expr>,
703    /// v7.38.19 — the target of a `SELECT … INTO <table>`, carried out
704    /// of `parse_bare_select` (which returns a `SelectStatement` and has
705    /// nowhere to put it) to the caller that lowers the pair to the CTAS
706    /// node. `bool` is `TEMP`.
707    pending_select_into: Option<(String, bool)>,
708    /// v7.39 (round 691) — collation lowering channel, the same shape as
709    /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
710    /// information, and `ast::OrderBy` is where this parser keeps ordering
711    /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
712    /// variant — puts a new arm on `eval_expr`, which this repo has
713    /// measured to overflow the debug stack. So while an ORDER BY KEY is
714    /// being parsed the postfix loop drops the name here instead of
715    /// refusing it, and the key's parser takes it.
716    ///
717    /// Only inside an ORDER BY key: everywhere else an unperformable
718    /// collation still errors, because accepting one at a COMPARISON and
719    /// ignoring it is the defect F36 exists to close.
720    in_order_by_key: bool,
721    order_key_collation: Option<String>,
722    /// POSITION(sub IN str) — while parsing the needle, the IN
723    /// keyword is the argument separator, not a membership test.
724    /// The postfix loop leaves IN unconsumed when this is set.
725    suppress_in_tail: bool,
726    /// Index of the token the last `advance()` returned — see
727    /// [`Parser::consumed_pos`].
728    last_consumed: usize,
729    /// v7.39 (round 506) — the statement's own text and the byte each token
730    /// starts at, so a MySQL projection item can report the SOURCE TEXT
731    /// MariaDB reports: `SELECT a  +  b` names its column `a  +  b`,
732    /// spacing and all. Only filled for a MySQL session — a PG one names
733    /// columns from the parsed shape and pays nothing for this.
734    src: Option<(String, Vec<usize>)>,
735}
736
737/// Max expr/select parser nesting (parens, subqueries, CASE, …).
738/// Real SQL nests a few dozen levels at the extreme. Each nesting level
739/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
740/// exists to turn a deep statement into a catchable parse ERROR: a stack
741/// overflow is an abort, and in the server it does not fail one query, it
742/// takes the process down and every other connection with it.
743///
744/// v7.39 (round 507) — measured, because the figure here used to be a
745/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
746/// in BOTH debug and release"), and the debug half of that is wrong by
747/// more than an order of magnitude:
748///
749///   * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
750///     this budget and errors. Verified against a live server for nested
751///     derived tables, parens, calls, CASE, IN-subqueries, scalar
752///     subqueries, NOT and unary minus — the server stayed up through all
753///     of them. This is the contract that matters, and it holds.
754///   * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
755///     LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
756///     and executing aborts around 8 inside a test thread. The budget is
757///     simply unreachable there, which is why a deep-nesting test has to
758///     ask for a large stack of its own — see `nesting_budget_errors_at`
759///     in the parser tests.
760/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
761/// one place.
762///
763/// There were two copies of this fact: a curated list, used for BARE
764/// names, and — in `try_peek_meta_qualified` — no list at all, which
765/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
766/// the engine to complain about a view it could not materialise. So
767/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
768/// had rows, `pg_catalog.pg_stat_activity` was an error.
769///
770/// PG puts `pg_catalog` at the implicit front of every search_path, so
771/// the two spellings name the same relation and must resolve the same
772/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
773/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
774/// meta_view_result path under their own names and must not be
775/// rewritten; a name that is neither reaches the ordinary resolver,
776/// which reports that the relation does not exist — PG's answer.
777const SYNTHESISED_PG_CATALOGS: &[&str] = &[
778    "pg_am",
779    "pg_attrdef",
780    "pg_attribute",
781    "pg_cast",
782    "pg_db_role_setting",
783    "pg_conversion",
784    "pg_default_acl",
785    "pg_shadow",
786    "pg_sequences",
787    "pg_range",
788    "pg_partitioned_table",
789    "pg_language",
790    "pg_group",
791    "pg_authid",
792    "pg_class",
793    "pg_collation",
794    "pg_constraint",
795    "pg_database",
796    "pg_depend",
797    "pg_amop",
798    "pg_amproc",
799    "pg_opclass",
800    "pg_opfamily",
801    // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
802    "pg_description",
803    "pg_enum",
804    "pg_extension",
805    // v7.39 (round 541) — pg_dump reads it for every relation of kind
806    // 'f'. SPG has no foreign tables, so it is empty, which is also
807    // what PG reports on a database that has none.
808    "pg_foreign_table",
809    // v7.39 (round 541) — the empty-by-truth family; see
810    // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
811    "pg_event_trigger",
812    "pg_file_settings",
813    "pg_foreign_data_wrapper",
814    "pg_foreign_server",
815    "pg_hba_file_rules",
816    "pg_ident_file_mappings",
817    "pg_init_privs",
818    "pg_parameter_acl",
819    "pg_prepared_xacts",
820    "pg_publication_namespace",
821    "pg_publication_rel",
822    "pg_publication_tables",
823    "pg_replication_origin",
824    "pg_replication_origin_status",
825    "pg_seclabel",
826    "pg_seclabels",
827    "pg_shdepend",
828    "pg_shdescription",
829    "pg_shmem_allocations",
830    "pg_shmem_allocations_numa",
831    "pg_shseclabel",
832    "pg_statistic_ext_data",
833    "pg_stats_ext",
834    "pg_stats_ext_exprs",
835    "pg_subscription_rel",
836    "pg_transform",
837    "pg_user_mapping",
838    "pg_user_mappings",
839    "pg_index",
840    "pg_indexes",
841    "pg_inherits",
842    // v7.39 (round 650) — the text-search catalogs SPG can fill
843    // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
844    // token types to dictionaries and SPG has no token-type model,
845    // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
846    "pg_ts_config",
847    "pg_ts_config_map",
848    "pg_ts_dict",
849    "pg_ts_parser",
850    "pg_ts_template",
851    "pg_matviews",
852    "pg_namespace",
853    // v7.39 (round 621)
854    "pg_operator",
855    "pg_policies",
856    "pg_policy",
857    "pg_proc",
858    "pg_publication",
859    "pg_replication_slots",
860    "pg_roles",
861    // v7.39 (round 143) — the rewrite-rule listing view.
862    // v7.39 (round 312) — and the rule catalogue itself, which
863    // `pg_get_ruledef(oid)` resolves against.
864    "pg_rewrite",
865    "pg_rules",
866    "pg_sequence",
867    "pg_settings",
868    "pg_stat_archiver",
869    "pg_stat_bgwriter",
870    "pg_stat_checkpointer",
871    "pg_stat_database",
872    "pg_stat_io",
873    "pg_stat_progress_analyze",
874    "pg_auth_members",
875    "pg_stat_progress_create_index",
876    "pg_stat_progress_vacuum",
877    "pg_stat_replication",
878    "pg_stat_slru",
879    "pg_stat_subscription_stats",
880    "pg_stat_user_functions",
881    "pg_stat_user_indexes",
882    "pg_stat_user_tables",
883    "pg_stat_wal",
884    "pg_prepared_statements",
885    "pg_largeobject",
886    "pg_largeobject_metadata",
887    "pg_statistic",
888    "pg_statistic_ext",
889    // v7.38.18 — the readable view over pg_statistic.
890    "pg_stats",
891    "pg_subscription",
892    "pg_tables",
893    "pg_tablespace",
894    // v7.39 (round 502) — the timezone catalogues. SPG resolved
895    // named zones correctly but could not list them, so a client
896    // populating a timezone picker got "relation does not exist".
897    "pg_timezone_abbrevs",
898    "pg_timezone_names",
899    "pg_trigger",
900    "pg_type",
901    "pg_user",
902    "pg_views",
903];
904
905const MAX_NEST_DEPTH: usize = 64;
906
907/// Stack accounting for the nesting budget, test-only.
908///
909/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
910/// that MOVES: a compiler upgrade grew the parser's debug frames and
911/// silently ate the margin until `nesting_budget_errors_cleanly` went
912/// from erroring cleanly to aborting on a stack overflow. A count
913/// cannot notice that on its own, so the budget is measured here and
914/// held to a ceiling.
915///
916/// The reading has to come from a helper whose OWN frame is the same at
917/// every call: debug slot placement does not follow source order, so a
918/// local's address inside the function under test is not that
919/// function's frame boundary. Two earlier probes were wrong that way —
920/// one read `&self.nest_depth`, which is the `Parser`'s address and
921/// never moves at all.
922#[cfg(test)]
923mod frame_meter {
924    extern crate std;
925    use std::cell::Cell;
926
927    // Per-THREAD, not global. `cargo test` runs tests in parallel and
928    // plenty of them parse nested expressions, so shared statics get
929    // stack addresses from several threads at once and the subtraction
930    // below turns into noise — it read 229,772 bytes per level that way,
931    // while passing when the test was run on its own.
932    std::thread_local! {
933        static AT_LO: Cell<usize> = const { Cell::new(0) };
934        static AT_HI: Cell<usize> = const { Cell::new(0) };
935    }
936
937    pub(super) const SAMPLE_LO: usize = 4;
938    pub(super) const SAMPLE_HI: usize = 24;
939
940    #[inline(never)]
941    pub(super) fn record(depth: usize) {
942        let anchor = 0u8;
943        let at = core::ptr::from_ref(&anchor) as usize;
944        if depth == SAMPLE_LO {
945            AT_LO.with(|c| c.set(at));
946        } else if depth == SAMPLE_HI {
947            AT_HI.with(|c| c.set(at));
948        }
949    }
950
951    /// Bytes of stack one nesting level costs, averaged over the span.
952    pub(super) fn bytes_per_level() -> usize {
953        let lo = AT_LO.with(Cell::get);
954        let hi = AT_HI.with(Cell::get);
955        assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
956        assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
957        (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
958    }
959
960    pub(super) fn reset() {
961        AT_LO.with(|c| c.set(0));
962        AT_HI.with(|c| c.set(0));
963    }
964}
965
966/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
967/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
968#[inline(never)]
969fn build_center_call(e: Expr) -> Expr {
970    Expr::FunctionCall {
971        name: alloc::string::String::from("center"),
972        args: alloc::vec![e],
973    }
974}
975
976/// Max consecutive binary operators at ONE precedence level
977/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
978/// parse time but evaluates and drops recursively — depth beyond
979/// this overflows 2 MiB worker stacks (debug eval frames run
980/// multiple KiB). `IN (…)` lists are flat and unaffected.
981const MAX_BINARY_CHAIN: usize = 256;
982
983/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
984/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
985/// it keeps its dedicated path (`parse_table_level_fk`).
986enum NamedTableConstraintKind {
987    Check,
988    Unique,
989    PrimaryKey,
990    Exclude,
991}
992
993impl Parser {
994    fn new(tokens: Vec<Token>) -> Self {
995        Self::new_with_dialect(tokens, false)
996    }
997
998    fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
999        Self {
1000            tokens,
1001            mysql_dialect,
1002            in_order_by_key: false,
1003            order_key_collation: None,
1004            pos: 0,
1005            nest_depth: 0,
1006            pending_sample_preds: Vec::new(),
1007            pending_select_into: None,
1008            suppress_in_tail: false,
1009            last_consumed: 0,
1010            src: None,
1011        }
1012    }
1013
1014    /// Hand the parser the text it is parsing, for [`Parser::source_span`].
1015    fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
1016        if self.mysql_dialect {
1017            self.src = Some((input.to_string(), offsets.to_vec()));
1018        }
1019        self
1020    }
1021
1022    /// The source text spanning tokens `start ..= end`, trimmed.
1023    ///
1024    /// The offsets are token STARTS, so the span runs to the start of the
1025    /// token after `end` and gives back the whitespace between them —
1026    /// trimming is what makes `a + b FROM t` end at `b`.
1027    fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1028        let (text, offsets) = self.src.as_ref()?;
1029        let from = *offsets.get(start)?;
1030        let to = *offsets.get(end + 1)?;
1031        text.get(from..to).map(str::trim_end)
1032    }
1033
1034    /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1035    /// nesting depth, erroring out cleanly past the budget.
1036    fn enter_nested(&mut self) -> Result<(), ParseError> {
1037        self.nest_depth += 1;
1038        #[cfg(test)]
1039        frame_meter::record(self.nest_depth);
1040        if self.nest_depth > MAX_NEST_DEPTH {
1041            self.nest_depth -= 1;
1042            return Err(self.err(alloc::format!(
1043                "statement nests deeper than {MAX_NEST_DEPTH} levels"
1044            )));
1045        }
1046        Ok(())
1047    }
1048
1049    fn peek(&self) -> &Token {
1050        // tokens always ends with Eof; pos is clamped in advance().
1051        &self.tokens[self.pos]
1052    }
1053
1054    fn advance(&mut self) -> Token {
1055        let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1056        self.last_consumed = self.pos;
1057        if self.pos + 1 < self.tokens.len() {
1058            self.pos += 1;
1059        }
1060        t
1061    }
1062
1063    /// v7.39 (round 340, V56) — the index of the token `advance()` just
1064    /// returned. It was computed as `pos - 1`, which is wrong at both
1065    /// ends: `advance()` parks on the final Eof rather than running off
1066    /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1067    /// input`), and after backtracking `pos` is no longer one past the
1068    /// token that failed. Recorded by `advance()` itself instead.
1069    const fn consumed_pos(&self) -> usize {
1070        self.last_consumed
1071    }
1072
1073    fn err(&self, message: String) -> ParseError {
1074        ParseError {
1075            message,
1076            token_pos: self.pos,
1077        }
1078    }
1079
1080    fn expect_eof(&self) -> Result<(), ParseError> {
1081        if matches!(self.peek(), Token::Eof) {
1082            Ok(())
1083        } else {
1084            Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1085        }
1086    }
1087
1088    /// v7.14.0 — swallow every token up to (but not including) the
1089    /// next semicolon / EOF. Used by the dump-noise dispatcher
1090    /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1091    /// etc. without modeling each grammar.
1092    /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1093    /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1094    /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1095    /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1096    /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1097    fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1098        let start = self.pos;
1099        self.advance(); // COMMENT
1100        if !matches!(self.peek(), Token::On) {
1101            self.pos = start;
1102            self.consume_until_statement_boundary();
1103            return Ok(Statement::Empty);
1104        }
1105        self.advance(); // ON
1106        let kind = match self.peek() {
1107            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1108            Token::Table => "table".into(),
1109            _ => {
1110                self.consume_until_statement_boundary();
1111                return Ok(Statement::Empty);
1112            }
1113        };
1114        if !matches!(
1115            kind.as_str(),
1116            "table"
1117                | "column"
1118                | "index"
1119                | "view"
1120                | "sequence"
1121                | "schema"
1122                | "type"
1123                | "database"
1124                | "function"
1125        ) {
1126            self.consume_until_statement_boundary();
1127            return Ok(Statement::Empty);
1128        }
1129        self.advance(); // the kind keyword
1130        // The object name. ⚠️ `expect_ident_like` strips a leading
1131        // `<schema>.` qualifier and returns only the trailing ident (SPG is
1132        // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1133        // `c`. Read the dotted parts from raw tokens instead, then let a
1134        // 3-part `schema.t.c` drop its leading schema like everywhere else.
1135        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1136        loop {
1137            match self.advance() {
1138                Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1139                other if unreserved_keyword_text(&other).is_some() => {
1140                    parts.push(unreserved_keyword_text(&other).unwrap());
1141                }
1142                other => {
1143                    return Err(ParseError {
1144                        message: alloc::format!("expected identifier, got {other:?}"),
1145                        token_pos: self.consumed_pos(),
1146                    });
1147                }
1148            }
1149            if matches!(self.peek(), Token::Dot) {
1150                self.advance();
1151            } else {
1152                break;
1153            }
1154        }
1155        // COLUMN wants `table.column`; every other kind wants a bare name.
1156        let want = if kind == "column" { 2 } else { 1 };
1157        while parts.len() > want {
1158            parts.remove(0);
1159        }
1160        let name = parts.join(".");
1161        // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1162        // pg_dump writes the SIGNATURE, and the paren list was a syntax
1163        // error here — a dump carrying one function comment failed to
1164        // restore. The list is consumed (the comment store keys by name;
1165        // overload-precise comments are the function-predicate follow-up).
1166        if matches!(self.peek(), Token::LParen)
1167            && matches!(
1168                kind.as_str(),
1169                "function" | "procedure" | "aggregate" | "routine"
1170            )
1171        {
1172            let mut depth = 0usize;
1173            loop {
1174                match self.advance() {
1175                    Token::LParen => depth += 1,
1176                    Token::RParen => {
1177                        depth -= 1;
1178                        if depth == 0 {
1179                            break;
1180                        }
1181                    }
1182                    Token::Eof => {
1183                        return Err(self.err(alloc::string::String::from(
1184                            "unterminated argument list in COMMENT ON",
1185                        )));
1186                    }
1187                    _ => {}
1188                }
1189            }
1190        }
1191        // `IS`
1192        if !matches!(self.peek(), Token::Is) {
1193            self.expect_keyword_ident("is")?;
1194        } else {
1195            self.advance();
1196        }
1197        let comment = match self.peek() {
1198            Token::Null => {
1199                self.advance();
1200                None
1201            }
1202            _ => Some(self.expect_string_literal()?),
1203        };
1204        Ok(Statement::CommentOn {
1205            kind,
1206            name,
1207            comment,
1208        })
1209    }
1210
1211    /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1212    /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1213    /// [CASCADE|RESTRICT]`.
1214    ///
1215    /// TABLE privileges are the real ones (stored, enforced, introspectable).
1216    /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1217    /// and the no-ON `GRANT role TO role` membership form — parses into
1218    /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1219    /// on them still restores.
1220    fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1221        self.advance(); // GRANT / REVOKE
1222        // REVOKE's optional `GRANT OPTION FOR` prefix.
1223        let mut grant_option = false;
1224        if !grant
1225            && self.peek_keyword_ident("grant")
1226            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1227        {
1228            self.advance(); // GRANT
1229            self.advance(); // OPTION
1230            self.expect_keyword_ident("for")?;
1231            grant_option = true;
1232        }
1233        // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1234        // words each with an optional COLUMN list.
1235        let mut privileges: Vec<GrantPriv> = Vec::new();
1236        if matches!(self.peek(), Token::All) {
1237            self.advance();
1238            if self.peek_keyword_ident("privileges") {
1239                self.advance();
1240            }
1241            // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1242            // column only.
1243            if matches!(self.peek(), Token::LParen) {
1244                let columns = self.parse_grant_column_list()?;
1245                privileges.push(GrantPriv {
1246                    word: "ALL".into(),
1247                    columns,
1248                });
1249            }
1250        } else {
1251            loop {
1252                // SELECT and INSERT lex as reserved tokens, so they never
1253                // reach `expect_ident_like` as plain idents; the rest
1254                // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1255                // MAINTAIN) are ordinary identifiers.
1256                let w = match self.peek() {
1257                    Token::Select => {
1258                        self.advance();
1259                        "SELECT".to_string()
1260                    }
1261                    Token::Insert => {
1262                        self.advance();
1263                        "INSERT".to_string()
1264                    }
1265                    // v7.39 (read01 round 60) — CREATE is a privilege word on a
1266                    // schema / database, and it lexes as a reserved token.
1267                    Token::Create => {
1268                        self.advance();
1269                        "CREATE".to_string()
1270                    }
1271                    // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1272                    // alice`) these "privilege words" are ROLE NAMES, and a
1273                    // role name is case-sensitive. `priv_from_word` folds case
1274                    // itself when they really are privileges.
1275                    _ => self.expect_ident_like()?,
1276                };
1277                // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1278                // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1279                let columns = if matches!(self.peek(), Token::LParen) {
1280                    self.parse_grant_column_list()?
1281                } else {
1282                    Vec::new()
1283                };
1284                privileges.push(GrantPriv { word: w, columns });
1285                if matches!(self.peek(), Token::Comma) {
1286                    self.advance();
1287                } else {
1288                    break;
1289                }
1290            }
1291        }
1292        // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1293        // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1294        if !matches!(self.peek(), Token::On) {
1295            let roles: Vec<String> = core::mem::take(&mut privileges)
1296                .into_iter()
1297                .map(|p| p.word)
1298                .collect();
1299            let grantees = self.parse_grantee_list(grant)?;
1300            // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1301            // no admin-option layer: a member cannot re-grant).
1302            self.consume_until_statement_boundary();
1303            return Ok(finish_grant(
1304                grant,
1305                GrantStatement {
1306                    privileges: Vec::new(),
1307                    object: GrantObject::Roles(roles),
1308                    grantees,
1309                    grant_option,
1310                },
1311            ));
1312        }
1313        self.advance(); // ON
1314        // An optional object-class keyword. `TABLE` (or no keyword at all) is
1315        // the enforced case; anything else parses and no-ops.
1316        let mut class = "TABLE";
1317        match self.peek() {
1318            Token::Table => {
1319                self.advance();
1320            }
1321            Token::All => {
1322                // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1323                // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1324                // IN SCHEMA` stay no-ops and keep their own object class.
1325                self.advance(); // ALL
1326                let kind = match self.peek() {
1327                    Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1328                    // TABLES has its own token (SHOW TABLES owns it).
1329                    Token::Tables | Token::Table => "tables".to_string(),
1330                    _ => String::new(),
1331                };
1332                if !kind.is_empty() {
1333                    self.advance();
1334                }
1335                // `IN SCHEMA <name>`
1336                if matches!(self.peek(), Token::In) {
1337                    self.advance();
1338                    if self.peek_keyword_ident("schema") {
1339                        self.advance();
1340                        let _schema = self.expect_ident_like()?;
1341                    }
1342                }
1343                if kind != "tables" {
1344                    self.consume_until_statement_boundary();
1345                    return Ok(finish_grant(
1346                        grant,
1347                        GrantStatement {
1348                            privileges,
1349                            object: GrantObject::Other("ALL … IN SCHEMA".into()),
1350                            grantees: Vec::new(),
1351                            grant_option,
1352                        },
1353                    ));
1354                }
1355                let grantees = self.parse_grantee_list(grant)?;
1356                self.consume_until_statement_boundary();
1357                return Ok(finish_grant(
1358                    grant,
1359                    GrantStatement {
1360                        privileges,
1361                        object: GrantObject::AllTablesInSchema,
1362                        grantees,
1363                        grant_option,
1364                    },
1365                ));
1366            }
1367            Token::Ident(w) | Token::QuotedIdent(w) => {
1368                let lc = w.to_ascii_lowercase();
1369                // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1370                // real objects with real ACLs now.
1371                if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1372                    self.advance();
1373                    let mut names: Vec<String> = Vec::new();
1374                    loop {
1375                        let mut parts: Vec<String> = Vec::new();
1376                        loop {
1377                            parts.push(self.expect_ident_like()?);
1378                            if matches!(self.peek(), Token::Dot) {
1379                                self.advance();
1380                            } else {
1381                                break;
1382                            }
1383                        }
1384                        names.push(parts.pop().expect("at least one part"));
1385                        if matches!(self.peek(), Token::Comma) {
1386                            self.advance();
1387                        } else {
1388                            break;
1389                        }
1390                    }
1391                    let grantees = self.parse_grantee_list(grant)?;
1392                    let mut grant_option = grant_option;
1393                    if grant && self.peek_keyword_ident("with") {
1394                        self.advance();
1395                        self.expect_keyword_ident("grant")?;
1396                        self.expect_keyword_ident("option")?;
1397                        grant_option = true;
1398                    }
1399                    self.consume_until_statement_boundary();
1400                    let object = match lc.as_str() {
1401                        "sequence" => GrantObject::Sequences(names),
1402                        "schema" => GrantObject::Schemas(names),
1403                        _ => GrantObject::Databases(names),
1404                    };
1405                    return Ok(finish_grant(
1406                        grant,
1407                        GrantStatement {
1408                            privileges,
1409                            object,
1410                            grantees,
1411                            grant_option,
1412                        },
1413                    ));
1414                }
1415                // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1416                // keys functions by NAME, so the argument list parses and is
1417                // dropped (an overload set shares one ACL — recorded residual).
1418                if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1419                    self.advance();
1420                    let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1421                    loop {
1422                        let mut parts: Vec<String> = Vec::new();
1423                        loop {
1424                            parts.push(self.expect_ident_like()?);
1425                            if matches!(self.peek(), Token::Dot) {
1426                                self.advance();
1427                            } else {
1428                                break;
1429                            }
1430                        }
1431                        let fname = parts.pop().expect("at least one part");
1432                        // v7.39 (read01 round 62) — the signature picks the
1433                        // overload, so it is captured.
1434                        let sig = if matches!(self.peek(), Token::LParen) {
1435                            Some(self.parse_function_signature_types()?)
1436                        } else {
1437                            None
1438                        };
1439                        names.push((fname, sig));
1440                        if matches!(self.peek(), Token::Comma) {
1441                            self.advance();
1442                        } else {
1443                            break;
1444                        }
1445                    }
1446                    let grantees = self.parse_grantee_list(grant)?;
1447                    self.consume_until_statement_boundary();
1448                    return Ok(finish_grant(
1449                        grant,
1450                        GrantStatement {
1451                            privileges,
1452                            object: GrantObject::Functions(names),
1453                            grantees,
1454                            grant_option,
1455                        },
1456                    ));
1457                }
1458                if matches!(
1459                    lc.as_str(),
1460                    "type"
1461                        | "domain"
1462                        | "language"
1463                        | "tablespace"
1464                        | "large"
1465                        | "foreign"
1466                        | "parameter"
1467                ) {
1468                    self.consume_until_statement_boundary();
1469                    return Ok(finish_grant(
1470                        grant,
1471                        GrantStatement {
1472                            privileges,
1473                            object: GrantObject::Other(lc.to_ascii_uppercase()),
1474                            grantees: Vec::new(),
1475                            grant_option,
1476                        },
1477                    ));
1478                }
1479                class = "TABLE";
1480            }
1481            _ => {}
1482        }
1483        let _ = class;
1484        // The table list. Schema-qualified names drop their qualifier (SPG is
1485        // single-schema) — but read the dotted parts from raw tokens, since
1486        // `expect_ident_like` would silently swallow the leading part.
1487        let mut tables: Vec<String> = Vec::new();
1488        loop {
1489            let mut parts: Vec<String> = Vec::new();
1490            loop {
1491                parts.push(self.expect_ident_like()?);
1492                if matches!(self.peek(), Token::Dot) {
1493                    self.advance();
1494                } else {
1495                    break;
1496                }
1497            }
1498            tables.push(parts.pop().expect("at least one part"));
1499            if matches!(self.peek(), Token::Comma) {
1500                self.advance();
1501            } else {
1502                break;
1503            }
1504        }
1505        let grantees = self.parse_grantee_list(grant)?;
1506        if grant && self.peek_keyword_ident("with") {
1507            self.advance();
1508            self.expect_keyword_ident("grant")?;
1509            self.expect_keyword_ident("option")?;
1510            grant_option = true;
1511        }
1512        // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1513        // to cascade to (no re-granting), so both are accepted and ignored.
1514        if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1515            self.advance();
1516        }
1517        Ok(finish_grant(
1518            grant,
1519            GrantStatement {
1520                privileges,
1521                object: GrantObject::Tables(tables),
1522                grantees,
1523                grant_option,
1524            },
1525        ))
1526    }
1527
1528    /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1529    /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1530    /// words; the caller normalises them into a signature key.
1531    fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1532        self.advance(); // (
1533        let mut types: Vec<String> = Vec::new();
1534        if matches!(self.peek(), Token::RParen) {
1535            self.advance();
1536            return Ok(types);
1537        }
1538        loop {
1539            // Collect the words of one argument up to a comma / close paren.
1540            let mut words: Vec<String> = Vec::new();
1541            loop {
1542                match self.peek() {
1543                    Token::Comma | Token::RParen | Token::Eof => break,
1544                    _ => {}
1545                }
1546                let tok = self.advance();
1547                match tok {
1548                    Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1549                    other => {
1550                        if let Some(w) = unreserved_keyword_text(&other) {
1551                            words.push(w);
1552                        }
1553                    }
1554                }
1555            }
1556            // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1557            // themselves several words (`double precision`, `character
1558            // varying`, `timestamp with time zone`), so "two words means the
1559            // first is a parameter name" reads the type off `f(double
1560            // precision)` as `precision`. v7.39 (round 282): recognise the
1561            // multi-word spellings first — a leading word that STARTS one of
1562            // them is part of the type, not a name.
1563            let joined = words.join(" ");
1564            let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1565                joined
1566            } else if words.len() >= 2 {
1567                words[1..].join(" ")
1568            } else {
1569                words.first().cloned().unwrap_or_default()
1570            };
1571            types.push(ty);
1572            if matches!(self.peek(), Token::Comma) {
1573                self.advance();
1574            } else {
1575                break;
1576            }
1577        }
1578        if matches!(self.peek(), Token::RParen) {
1579            self.advance();
1580        }
1581        Ok(types)
1582    }
1583
1584    /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1585    fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1586        self.advance(); // (
1587        let mut cols = Vec::new();
1588        loop {
1589            cols.push(self.expect_ident_like()?);
1590            if matches!(self.peek(), Token::Comma) {
1591                self.advance();
1592            } else {
1593                break;
1594            }
1595        }
1596        if !matches!(self.peek(), Token::RParen) {
1597            return Err(self.err(alloc::format!(
1598                "expected ')' to close the column list, got {:?}",
1599                self.peek()
1600            )));
1601        }
1602        self.advance(); // )
1603        Ok(cols)
1604    }
1605
1606    /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1607    /// PUBLIC.
1608    fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1609        if grant {
1610            if matches!(self.peek(), Token::To) {
1611                self.advance();
1612            } else {
1613                self.expect_keyword_ident("to")?;
1614            }
1615        } else if matches!(self.peek(), Token::From) {
1616            self.advance();
1617        } else {
1618            self.expect_keyword_ident("from")?;
1619        }
1620        let mut grantees: Vec<String> = Vec::new();
1621        loop {
1622            // `GROUP name` is the legacy spelling of a plain role name.
1623            if self.peek_keyword_ident("group") {
1624                self.advance();
1625            }
1626            if self.peek_keyword_ident("public") {
1627                self.advance();
1628                grantees.push(String::new()); // PUBLIC
1629            } else {
1630                grantees.push(self.expect_ident_like()?);
1631            }
1632            if matches!(self.peek(), Token::Comma) {
1633                self.advance();
1634            } else {
1635                break;
1636            }
1637        }
1638        Ok(grantees)
1639    }
1640
1641    /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1642    /// The body keeps its `$N` placeholders; substitution happens at
1643    /// EXECUTE. The declared types are recorded for
1644    /// `pg_prepared_statements.parameter_types` but are not enforced —
1645    /// PG infers when the list is omitted, and SPG resolves the values
1646    /// at substitution time either way.
1647    fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1648        let start = self.pos;
1649        self.advance(); // PREPARE
1650        // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1651        // different statement that happens to share the keyword. PG
1652        // ships with `max_prepared_transactions = 0` and reports it
1653        // this way; SPG has no prepared-transaction registry, so the
1654        // same wording is the accurate answer rather than a dodge.
1655        // Round 277 turned this from a silent no-op into a confusing
1656        // "expected AS in PREPARE" parse error.
1657        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1658            self.advance();
1659            let gid = match self.advance() {
1660                Token::String(g) => g,
1661                other => {
1662                    return Err(self.err(alloc::format!(
1663                        "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1664                    )));
1665                }
1666            };
1667            return Ok(Statement::PrepareTransaction(gid));
1668        }
1669        let name = self.expect_ident_like()?;
1670        let mut param_types = Vec::new();
1671        if matches!(self.peek(), Token::LParen) {
1672            self.advance();
1673            loop {
1674                let mut ty = self.expect_ident_like()?;
1675                // A parameterised type name (`numeric(10,2)`,
1676                // `varchar(20)`) keeps its argument list in the text.
1677                if matches!(self.peek(), Token::LParen) {
1678                    let mut depth = 0usize;
1679                    let mut buf = String::from("(");
1680                    loop {
1681                        match self.advance() {
1682                            Token::LParen => {
1683                                depth += 1;
1684                                if depth > 1 {
1685                                    buf.push('(');
1686                                }
1687                            }
1688                            Token::RParen => {
1689                                depth -= 1;
1690                                buf.push(')');
1691                                if depth == 0 {
1692                                    break;
1693                                }
1694                            }
1695                            Token::Comma => buf.push(','),
1696                            Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1697                            Token::Eof => break,
1698                            _ => {}
1699                        }
1700                    }
1701                    ty.push_str(&buf);
1702                }
1703                // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1704                // position, same family as the parameter list above.
1705                let array_suffix = self.consume_array_suffix();
1706                ty.push_str(&array_suffix);
1707                param_types.push(ty);
1708                match self.peek() {
1709                    Token::Comma => {
1710                        self.advance();
1711                    }
1712                    Token::RParen => {
1713                        self.advance();
1714                        break;
1715                    }
1716                    other => {
1717                        return Err(self.err(alloc::format!(
1718                            "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1719                        )));
1720                    }
1721                }
1722            }
1723        }
1724        if !matches!(self.peek(), Token::As) {
1725            return Err(self.err(alloc::format!(
1726                "expected AS in PREPARE, got {:?}",
1727                self.peek()
1728            )));
1729        }
1730        self.advance();
1731        let body = self.parse_one_statement()?;
1732        // The Parser holds tokens, not the source text, so the
1733        // statement PG reports in `pg_prepared_statements.statement`
1734        // is rebuilt from the AST rather than sliced from the input.
1735        let _ = start;
1736        let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1737        if !param_types.is_empty() {
1738            source.push_str(" (");
1739            source.push_str(&param_types.join(", "));
1740            source.push(')');
1741        }
1742        source.push_str(" AS ");
1743        source.push_str(&alloc::format!("{body}"));
1744        Ok(Statement::Prepare {
1745            name,
1746            param_types,
1747            body: alloc::boxed::Box::new(body),
1748            source,
1749        })
1750    }
1751
1752    /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1753    fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1754        self.advance(); // EXECUTE
1755        let name = self.expect_ident_like()?;
1756        let mut args = Vec::new();
1757        if matches!(self.peek(), Token::LParen) {
1758            self.advance();
1759            if matches!(self.peek(), Token::RParen) {
1760                self.advance();
1761            } else {
1762                loop {
1763                    args.push(self.parse_expr(0)?);
1764                    match self.advance() {
1765                        Token::Comma => {}
1766                        Token::RParen => break,
1767                        other => {
1768                            return Err(self.err(alloc::format!(
1769                                "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1770                            )));
1771                        }
1772                    }
1773                }
1774            }
1775        }
1776        Ok(Statement::Execute { name, args })
1777    }
1778
1779    /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1780    /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1781    /// procedure catalog yet, so this reports PG's not-found error
1782    /// (with its HINT) rather than pretending the call ran.
1783    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1784    /// Bare `DISCARD` is a syntax error in PG; so it is here.
1785    fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1786        self.advance(); // DISCARD
1787        let target = match self.advance() {
1788            Token::All => DiscardTarget::All,
1789            Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1790                "all" => DiscardTarget::All,
1791                "plans" => DiscardTarget::Plans,
1792                "sequences" => DiscardTarget::Sequences,
1793                "temp" | "temporary" => DiscardTarget::Temp,
1794                other => {
1795                    return Err(self.err(format!(
1796                        "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1797                    )));
1798                }
1799            },
1800            other => {
1801                return Err(self.err(format!(
1802                    "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1803                )));
1804            }
1805        };
1806        Ok(Statement::Discard(target))
1807    }
1808
1809    /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1810    /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1811    /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1812    /// aggressively the server interrupts, which SPG does not distinguish.
1813    /// Bare `KILL <id>` means CONNECTION.
1814    fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1815        self.advance(); // KILL
1816        let mut query_only = false;
1817        loop {
1818            // CONNECTION is a reserved keyword token (it also opens
1819            // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1820            // `Token::Connection` rather than a bare ident.
1821            if matches!(self.peek(), Token::Connection) {
1822                self.advance();
1823                break;
1824            }
1825            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1826                break;
1827            };
1828            match w.to_ascii_lowercase().as_str() {
1829                "hard" | "soft" => {
1830                    self.advance();
1831                }
1832                "query" => {
1833                    self.advance();
1834                    query_only = true;
1835                    break;
1836                }
1837                _ => break,
1838            }
1839        }
1840        let id = self.parse_expr(0)?;
1841        Ok(Statement::Kill {
1842            query_only,
1843            id: Box::new(id),
1844        })
1845    }
1846
1847    fn parse_call(&mut self) -> Result<Statement, ParseError> {
1848        self.advance(); // CALL
1849        let name = self.expect_ident_like()?;
1850        self.consume_until_statement_boundary();
1851        Ok(Statement::Call(name))
1852    }
1853
1854    fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1855        self.advance(); // DEALLOCATE
1856        // PG accepts an optional noise `PREPARE` keyword here.
1857        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1858            self.advance();
1859        }
1860        if matches!(self.peek(), Token::All) {
1861            self.advance();
1862            return Ok(Statement::Deallocate(None));
1863        }
1864        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1865            self.advance();
1866            return Ok(Statement::Deallocate(None));
1867        }
1868        let name = self.expect_ident_like()?;
1869        Ok(Statement::Deallocate(Some(name)))
1870    }
1871
1872    fn consume_until_statement_boundary(&mut self) {
1873        loop {
1874            match self.peek() {
1875                Token::Semicolon | Token::Eof => return,
1876                _ => self.advance(),
1877            };
1878        }
1879    }
1880
1881    /// v7.38.19 — the database name a `CREATE DATABASE` names, skipping
1882    /// an `IF NOT EXISTS`. Consumes only the name; the collation scanner
1883    /// runs after it and eats the rest.
1884    fn scan_database_name(&mut self) -> Option<String> {
1885        // The caller has only PEEKED at `DATABASE`; step past it, or the
1886        // first identifier found is the keyword itself. It was, and
1887        // `pg_database` listed a database called `database`.
1888        if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case("database"))
1889        {
1890            self.advance();
1891        }
1892        for kw in ["if", "not", "exists"] {
1893            if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case(kw))
1894            {
1895                self.advance();
1896            }
1897        }
1898        match self.peek().clone() {
1899            Token::Ident(w) | Token::QuotedIdent(w) => {
1900                self.advance();
1901                Some(w)
1902            }
1903            _ => None,
1904        }
1905    }
1906
1907    /// v7.38.18 — consume to the statement boundary like
1908    /// `consume_until_statement_boundary`, but pick out the collation a
1909    /// `CREATE DATABASE` asked for on the way.
1910    ///
1911    /// `LC_COLLATE 'de_DE.utf8'` and `LOCALE 'de_DE.utf8'` both count;
1912    /// `LC_CTYPE` does not, because SPG has no separate ctype and
1913    /// pretending to honour it would be the more misleading answer. An
1914    /// `=` between the keyword and the value is optional, as in PG.
1915    ///
1916    /// The whole statement used to be thrown away. Being single-database
1917    /// makes the NAME a no-op; it does not make the collation one.
1918    fn scan_database_collation_until_boundary(&mut self) -> Option<String> {
1919        let mut want_value = false;
1920        let mut found: Option<String> = None;
1921        loop {
1922            let tok = self.peek().clone();
1923            match &tok {
1924                Token::Semicolon | Token::Eof => break,
1925                Token::Ident(w) | Token::QuotedIdent(w)
1926                    if w.eq_ignore_ascii_case("lc_collate") || w.eq_ignore_ascii_case("locale") =>
1927                {
1928                    want_value = true;
1929                }
1930                Token::Eq if want_value => {}
1931                Token::String(v) if want_value => {
1932                    found = Some(v.clone());
1933                    want_value = false;
1934                }
1935                Token::Ident(v) | Token::QuotedIdent(v) if want_value => {
1936                    found = Some(v.clone());
1937                    want_value = false;
1938                }
1939                _ => want_value = false,
1940            }
1941            self.advance();
1942        }
1943        found
1944    }
1945
1946    /// v7.22 (round-13 T2) — consume to the statement boundary like
1947    /// `consume_until_statement_boundary`, but pick out the sequence
1948    /// name on the way: either `SEQUENCE NAME <ident>` (identity
1949    /// columns) or the first string literal (`nextval('<seq>')`).
1950    /// Schema qualifiers and `::regclass` casts are stripped.
1951    fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1952        let mut seq: Option<String> = None;
1953        let mut after_sequence_kw = false;
1954        let mut after_name_kw = false;
1955        loop {
1956            match self.peek().clone() {
1957                Token::Semicolon | Token::Eof => break,
1958                Token::Ident(s) | Token::QuotedIdent(s) => {
1959                    if after_name_kw && seq.is_none() {
1960                        self.advance();
1961                        let mut name = s;
1962                        // `SEQUENCE NAME public.groups_id_seq` — keep
1963                        // the bare name, drop qualifiers.
1964                        while matches!(self.peek(), Token::Dot) {
1965                            self.advance();
1966                            if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1967                                name = n;
1968                            }
1969                        }
1970                        seq = Some(name);
1971                        after_name_kw = false;
1972                        continue;
1973                    }
1974                    if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1975                        after_name_kw = true;
1976                        after_sequence_kw = false;
1977                    } else {
1978                        after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1979                    }
1980                    self.advance();
1981                }
1982                Token::String(s) => {
1983                    if seq.is_none() {
1984                        // `nextval('public.groups_id_seq'::regclass)`
1985                        let bare = s
1986                            .rsplit_once('.')
1987                            .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1988                        seq = Some(bare);
1989                    }
1990                    self.advance();
1991                }
1992                _ => {
1993                    after_sequence_kw = false;
1994                    after_name_kw = false;
1995                    self.advance();
1996                }
1997            }
1998        }
1999        seq
2000    }
2001
2002    /// v7.39 (round 621) — is the next token the keyword `BY`?
2003    ///
2004    /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
2005    /// column, table and alias name — and SPG lexed it into a dedicated
2006    /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
2007    /// two-letter keywords the lexer knew, this was the only one PG leaves
2008    /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
2009    ///
2010    /// The token is gone; the three clauses that own the word — GROUP BY,
2011    /// ORDER BY, PARTITION BY — and the handful of other places that expect it
2012    /// ask this instead. Adding it to the unreserved-identifier table was not
2013    /// enough on its own: identifier positions that match the token shape
2014    /// directly (an index's column list, a table alias) never consult that
2015    /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
2016    /// Not lexing it as a keyword closes the whole class rather than the two
2017    /// positions that happened to be noticed.
2018    fn peek_is_by(&self) -> bool {
2019        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
2020    }
2021
2022    /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
2023    /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
2024    /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
2025    fn consume_drop_behaviour(&mut self) {
2026        if matches!(
2027            self.peek(),
2028            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
2029        ) {
2030            self.advance();
2031        }
2032    }
2033
2034    fn expect_ident_like(&mut self) -> Result<String, ParseError> {
2035        let first = match self.advance() {
2036            Token::Ident(s) | Token::QuotedIdent(s) => s,
2037            // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
2038            // per PG's `pg_get_keywords()` classification. SPG tokenizes
2039            // these as named variants for parsing leverage in the
2040            // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
2041            // `BEGIN`, etc.), but they MUST still be usable as table /
2042            // column / alias names in DDL+DML. Sentori migrations like
2043            // 0001_init.sql ship `release TEXT NOT NULL` in the events
2044            // table — the `events.release` column carries the release
2045            // identifier string. Pre-T4 this triggered "expected
2046            // identifier, got Release" and blocked every drop-in user
2047            // whose schema had a column / alias with one of these names.
2048            other if unreserved_keyword_text(&other).is_some() => {
2049                unreserved_keyword_text(&other).unwrap()
2050            }
2051            other => {
2052                return Err(ParseError {
2053                    message: format!("expected identifier, got {other:?}"),
2054                    token_pos: self.consumed_pos(),
2055                });
2056            }
2057        };
2058        // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
2059        // qualify every name with `public.` (and pg_catalog.* for
2060        // functions); SPG is single-schema so we discard the
2061        // prefix and return only the trailing ident. Same shape
2062        // also handles MySQL `db.tbl` cross-database refs (SPG
2063        // ignores the db part).
2064        if matches!(self.peek(), Token::Dot) {
2065            self.advance();
2066            match self.advance() {
2067                Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
2068                other if unreserved_keyword_text(&other).is_some() => {
2069                    return Ok(unreserved_keyword_text(&other).unwrap());
2070                }
2071                other => {
2072                    return Err(ParseError {
2073                        message: format!("expected identifier after '{first}.', got {other:?}"),
2074                        token_pos: self.consumed_pos(),
2075                    });
2076                }
2077            }
2078        }
2079        Ok(first)
2080    }
2081
2082    #[allow(clippy::too_many_lines)]
2083    fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2084        // v7.14.0 — empty / comment-only / semicolon-only input
2085        // (after the lexer strips line + block + MySQL
2086        // conditional comments) lands as Statement::Empty.
2087        // pg_dump and mysqldump emit several wrappers that
2088        // collapse to nothing after stripping (`/*!40101 SET …
2089        // */;`, blank lines between statements); the engine
2090        // returns CommandOk no-op so the dump loads cleanly.
2091        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2092            return Ok(Statement::Empty);
2093        }
2094        // v7.14.0 — pg_dump / mysqldump "noise" statements:
2095        // catalog / metadata DDL that has no behavioural effect
2096        // on SPG's single-schema, single-database, single-user
2097        // model. Consume the whole statement up to the next
2098        // semicolon / EOF and return Empty. This is broader than
2099        // the per-keyword DROP / SET / COMMENT arms but lets the
2100        // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2101        // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2102        // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2103        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2104            let lc = s.to_ascii_lowercase();
2105            // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2106            if lc == "comment" {
2107                return self.parse_comment_on();
2108            }
2109            // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2110            if lc == "grant" || lc == "revoke" {
2111                return self.parse_grant_or_revoke(lc == "grant");
2112            }
2113            // v7.39 (round 277) — the SQL-level prepared-statement
2114            // surface is REAL now. It used to be accepted and dropped
2115            // on the theory that "real execution still happens via the
2116            // extended-query flow" — true only for a driver that uses
2117            // that flow; a plain SQL PREPARE / EXECUTE returned no
2118            // rows at all.
2119            if lc == "prepare" {
2120                return self.parse_prepare();
2121            }
2122            if lc == "execute" {
2123                return self.parse_execute();
2124            }
2125            if lc == "deallocate" {
2126                return self.parse_deallocate();
2127            }
2128            // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2129            // accepted and dropped, so an application's stored-procedure
2130            // invocation reported success and did nothing. SPG has no
2131            // procedure catalog, so every CALL names a procedure that
2132            // does not exist — which is exactly what PG says.
2133            if lc == "call" {
2134                return self.parse_call();
2135            }
2136            // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2137            // names one connection and acts on it.
2138            if lc == "kill" {
2139                return self.parse_kill();
2140            }
2141            if lc == "discard" {
2142                return self.parse_discard();
2143            }
2144            // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2145            // Still performs nothing; the roles are carried out so a name
2146            // that does not exist is refused, as PG18 refuses it.
2147            if lc == "reassign" {
2148                self.advance();
2149                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2150                    self.advance();
2151                }
2152                if self.peek_is_by() {
2153                    self.advance();
2154                }
2155                // Only the roles BEFORE the TO are the ones that must
2156                // exist — `TO` names the new owner, which PG checks as
2157                // well, so both lists are collected.
2158                let mut names = self.take_comma_separated_names();
2159                if matches!(self.peek(), Token::To) {
2160                    self.advance();
2161                    names.extend(self.take_comma_separated_names());
2162                }
2163                self.consume_until_statement_boundary();
2164                return Ok(Statement::ValidateOnly {
2165                    kind: crate::ast::ValidateOnlyKind::RoleName,
2166                    names,
2167                });
2168            }
2169            // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2170            // unconditionally with `no security label providers have been
2171            // loaded`, whatever object it names, because none is loaded.
2172            // SPG has none either; accepting it told the caller a label had
2173            // been applied when nothing anywhere records one.
2174            if lc == "security" {
2175                self.consume_until_statement_boundary();
2176                return Ok(Statement::ValidateOnly {
2177                    kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2178                    names: Vec::new(),
2179                });
2180            }
2181            if is_dump_noise_statement(&lc) {
2182                self.consume_until_statement_boundary();
2183                return Ok(Statement::Empty);
2184            }
2185        }
2186        match self.peek() {
2187            Token::Select => self.parse_select_stmt(),
2188            // v7.37.17 (17.6 siblings) — a statement opening with a
2189            // parenthesized query group: `(SELECT … UNION …)
2190            // INTERSECT …`. parse_bare_select's group arm consumes
2191            // the parens; the select parser handles the outer chain
2192            // and tail.
2193            Token::LParen
2194                if matches!(
2195                    self.tokens.get(self.pos + 1),
2196                    Some(Token::Select | Token::LParen | Token::Values)
2197                ) =>
2198            {
2199                self.parse_select_stmt()
2200            }
2201            // v7.37.17 (17.6 siblings) — top-level bare VALUES
2202            // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2203            // Lowers to the same UNION ALL chain the FROM-position
2204            // form uses, then reuses the shared SELECT tail.
2205            Token::Values => {
2206                self.advance(); // VALUES
2207                let mut head = self.parse_values_rows_body()?;
2208                self.parse_select_tail_into(&mut head)?;
2209                Ok(Statement::Select(head))
2210            }
2211            // SQL-standard `TABLE name` shorthand for
2212            // `SELECT * FROM name` — pg_dump never emits it, but
2213            // psql users and PG docs use it constantly. Set-op
2214            // chains and the ORDER BY/LIMIT tail compose like any
2215            // SELECT head.
2216            Token::Table
2217                if matches!(
2218                    self.tokens.get(self.pos + 1),
2219                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2220                ) =>
2221            {
2222                let mut head = self.parse_table_shorthand()?;
2223                self.parse_setop_chain_into(&mut head)?;
2224                self.parse_select_tail_into(&mut head)?;
2225                Ok(Statement::Select(head))
2226            }
2227            // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2228            // body is a dollar-quoted plpgsql block (lexer already
2229            // collapsed `$$…$$` into a single Token::String).
2230            // v7.16.2 — mailrs round-10 A.2: parse the body as a
2231            // real PlPgSqlBlock so the engine can EXECUTE it at
2232            // top level instead of silently swallowing. Pre-
2233            // v7.16.2 the parser threw the body away and the
2234            // engine returned CommandOk for the entire DO; that
2235            // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2236            // $$` into a SEV-1 silent no-op (the IF + the rename
2237            // were both invisible — mailrs's migrate-042 didn't
2238            // actually run). Now the body parses + executes;
2239            // EmbeddedSql inside the block runs immediately
2240            // against the engine (not deferred — we're at top
2241            // level, not inside a trigger row-write loop).
2242            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2243                self.advance();
2244                let body_text = match self.advance() {
2245                    Token::String(s) => s,
2246                    other => {
2247                        return Err(self.err(alloc::format!(
2248                            "expected dollar-quoted body after DO, got {other:?}"
2249                        )));
2250                    }
2251                };
2252                // Optional `LANGUAGE <name>` trailer (idents only).
2253                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2254                    self.advance();
2255                    let _ = self.expect_ident_like()?;
2256                }
2257                // Parse the body — same shape CREATE FUNCTION
2258                // uses for trigger function bodies. If the body
2259                // doesn't parse cleanly we surface the error
2260                // (better than silent no-op).
2261                let block = parse_plpgsql_body(&body_text)?;
2262                Ok(Statement::DoBlock(block))
2263            }
2264            // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2265            // WITH isn't a reserved token in our lexer — comes through
2266            // as `Token::Ident("with")` (case-insensitive).
2267            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2268                self.advance();
2269                self.parse_with_cte_then_select()
2270            }
2271            // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2272            // an identifier — not a reserved keyword.
2273            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2274                self.advance();
2275                let mut analyze = false;
2276                let mut suggest = false;
2277                let mut costs_off = false;
2278                let mut buffers = false;
2279                let mut timing_off = false;
2280                let mut settings = false;
2281                let mut wal = false;
2282                let mut summary_off = false;
2283                let mut format = crate::ast::ExplainFormat::Text;
2284                // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2285                // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2286                // options are comma-separated. Booleans default to ON
2287                // when the value token is omitted (matches PG).
2288                if matches!(self.peek(), Token::LParen) {
2289                    self.advance();
2290                    loop {
2291                        let opt = match self.peek().clone() {
2292                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2293                            other => {
2294                                return Err(self.err(format!(
2295                                    "expected option keyword inside EXPLAIN (…), got {other:?}"
2296                                )));
2297                            }
2298                        };
2299                        self.advance();
2300                        if opt.eq_ignore_ascii_case("suggest") {
2301                            suggest = true;
2302                            // SUGGEST takes no explicit value today.
2303                        } else if opt.eq_ignore_ascii_case("costs") {
2304                            // PG syntax: `COSTS [ON | OFF]`. Default
2305                            // when value omitted is ON, so plain
2306                            // `COSTS` is a no-op. `COSTS OFF` flips.
2307                            // `ON` lexes to `Token::On` (reserved
2308                            // keyword in JOIN ... ON contexts); accept
2309                            // it alongside the bare Ident form so the
2310                            // grammar matches PG verbatim.
2311                            let value = match self.peek().clone() {
2312                                Token::On => {
2313                                    self.advance();
2314                                    true
2315                                }
2316                                Token::Ident(v) | Token::QuotedIdent(v)
2317                                    if v.eq_ignore_ascii_case("off") =>
2318                                {
2319                                    self.advance();
2320                                    false
2321                                }
2322                                Token::Ident(v) | Token::QuotedIdent(v)
2323                                    if v.eq_ignore_ascii_case("true") =>
2324                                {
2325                                    self.advance();
2326                                    true
2327                                }
2328                                _ => true,
2329                            };
2330                            costs_off = !value;
2331                        } else if opt.eq_ignore_ascii_case("analyze")
2332                            || opt.eq_ignore_ascii_case("analyse")
2333                        {
2334                            // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2335                            // Same default-ON rule as ANALYZE keyword form.
2336                            let value = match self.peek().clone() {
2337                                Token::On => {
2338                                    self.advance();
2339                                    true
2340                                }
2341                                Token::Ident(v) | Token::QuotedIdent(v)
2342                                    if v.eq_ignore_ascii_case("off") =>
2343                                {
2344                                    self.advance();
2345                                    false
2346                                }
2347                                Token::Ident(v) | Token::QuotedIdent(v)
2348                                    if v.eq_ignore_ascii_case("true") =>
2349                                {
2350                                    self.advance();
2351                                    true
2352                                }
2353                                _ => true,
2354                            };
2355                            analyze = value;
2356                        } else if opt.eq_ignore_ascii_case("buffers") {
2357                            // v7.37.22 — `BUFFERS [ON|OFF]`.
2358                            let value = match self.peek().clone() {
2359                                Token::On => {
2360                                    self.advance();
2361                                    true
2362                                }
2363                                Token::Ident(v) | Token::QuotedIdent(v)
2364                                    if v.eq_ignore_ascii_case("off") =>
2365                                {
2366                                    self.advance();
2367                                    false
2368                                }
2369                                Token::Ident(v) | Token::QuotedIdent(v)
2370                                    if v.eq_ignore_ascii_case("true") =>
2371                                {
2372                                    self.advance();
2373                                    true
2374                                }
2375                                _ => true,
2376                            };
2377                            buffers = value;
2378                        } else if opt.eq_ignore_ascii_case("timing") {
2379                            // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2380                            // the measured wall-clock annotation.
2381                            let value = match self.peek().clone() {
2382                                Token::On => {
2383                                    self.advance();
2384                                    true
2385                                }
2386                                Token::Ident(v) | Token::QuotedIdent(v)
2387                                    if v.eq_ignore_ascii_case("off") =>
2388                                {
2389                                    self.advance();
2390                                    false
2391                                }
2392                                Token::Ident(v) | Token::QuotedIdent(v)
2393                                    if v.eq_ignore_ascii_case("true") =>
2394                                {
2395                                    self.advance();
2396                                    true
2397                                }
2398                                _ => true,
2399                            };
2400                            timing_off = !value;
2401                        } else if opt.eq_ignore_ascii_case("settings") {
2402                            settings = true;
2403                        } else if opt.eq_ignore_ascii_case("wal") {
2404                            wal = true;
2405                        } else if opt.eq_ignore_ascii_case("summary") {
2406                            // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2407                            // gates the trailing Planning/Execution Time
2408                            // lines now (was accept-and-no-op).
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                            summary_off = !value;
2429                        } else if opt.eq_ignore_ascii_case("verbose")
2430                            || opt.eq_ignore_ascii_case("format")
2431                        {
2432                            // v7.37.22 — accept-but-no-op the remaining
2433                            // PG options so EXPLAIN-using clients
2434                            // (pgAdmin / DataGrip) don't see syntax
2435                            // errors. FORMAT takes a value (text /
2436                            // json / yaml / xml); skip the next token
2437                            // if it's an ident.
2438                            if opt.eq_ignore_ascii_case("format") {
2439                                if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2440                                {
2441                                    self.advance();
2442                                    format = match v.to_ascii_lowercase().as_str() {
2443                                        "text" => crate::ast::ExplainFormat::Text,
2444                                        "json" => crate::ast::ExplainFormat::Json,
2445                                        "xml" => crate::ast::ExplainFormat::Xml,
2446                                        "yaml" => crate::ast::ExplainFormat::Yaml,
2447                                        other => {
2448                                            return Err(self.err(format!(
2449                                                "EXPLAIN (FORMAT …): unknown format {other:?}; \
2450                                                 supports text, json, xml, yaml"
2451                                            )));
2452                                        }
2453                                    };
2454                                }
2455                            } else {
2456                                // VERBOSE / SUMMARY take optional ON/OFF;
2457                                // consume if present.
2458                                if matches!(self.peek(), Token::On) {
2459                                    self.advance();
2460                                } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2461                                    self.peek().clone()
2462                                    && (v.eq_ignore_ascii_case("off")
2463                                        || v.eq_ignore_ascii_case("true"))
2464                                {
2465                                    self.advance();
2466                                    let _ = v;
2467                                }
2468                            }
2469                        } else {
2470                            return Err(self.err(format!(
2471                                "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2472                            )));
2473                        }
2474                        if matches!(self.peek(), Token::Comma) {
2475                            self.advance();
2476                            continue;
2477                        }
2478                        break;
2479                    }
2480                    if !matches!(self.peek(), Token::RParen) {
2481                        return Err(self.err(format!(
2482                            "expected ')' after EXPLAIN options, got {:?}",
2483                            self.peek()
2484                        )));
2485                    }
2486                    self.advance();
2487                } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2488                    && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2489                {
2490                    self.advance();
2491                    analyze = true;
2492                }
2493                // v7.39 (round 224) — the body may open with WITH (CTEs);
2494                // route through the same CTE-then-SELECT path the top-level
2495                // WITH statement uses. v7.39 (round 225) — DML bodies parse
2496                // too (PG explains INSERT / UPDATE / DELETE).
2497                let inner = match self.peek().clone() {
2498                    Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2499                        self.advance();
2500                        self.parse_with_cte_then_select()?
2501                    }
2502                    Token::Insert => self.parse_insert_stmt(false)?,
2503                    Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2504                        self.advance();
2505                        self.parse_update_after_keyword()?
2506                    }
2507                    Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2508                        self.advance();
2509                        self.parse_delete_after_keyword()?
2510                    }
2511                    _ => self.parse_select_stmt()?,
2512                };
2513                if !matches!(
2514                    inner,
2515                    Statement::Select(_)
2516                        | Statement::Insert(_)
2517                        | Statement::Update(_)
2518                        | Statement::Delete(_)
2519                ) {
2520                    return Err(self.err(format!(
2521                        "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2522                    )));
2523                }
2524                Ok(Statement::Explain(crate::ast::ExplainStatement {
2525                    analyze,
2526                    inner: Box::new(inner),
2527                    suggest,
2528                    costs_off,
2529                    buffers,
2530                    timing_off,
2531                    settings,
2532                    wal,
2533                    summary_off,
2534                    format,
2535                }))
2536            }
2537            Token::Create => self.parse_create_stmt(),
2538            Token::Insert => self.parse_insert_stmt(false),
2539            // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2540            // spelling; route to the same handler. DESC is the
2541            // reserved ORDER BY token, so it gets its own arm.
2542            Token::Ident(s)
2543                if s.eq_ignore_ascii_case("describe")
2544                    && matches!(
2545                        self.tokens.get(self.pos + 1),
2546                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2547                    ) =>
2548            {
2549                self.advance();
2550                let table = self.expect_ident_like()?;
2551                Ok(Statement::ShowColumns(table))
2552            }
2553            Token::Desc
2554                if matches!(
2555                    self.tokens.get(self.pos + 1),
2556                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2557                ) =>
2558            {
2559                self.advance();
2560                let table = self.expect_ident_like()?;
2561                Ok(Statement::ShowColumns(table))
2562            }
2563            // `COPY table [(cols)] TO STDOUT` — the export half of
2564            // pg_dump's COPY pair (the FROM stdin half rides the
2565            // embed import path). Options need a format design and
2566            // error honestly.
2567            Token::Ident(s)
2568                if s.eq_ignore_ascii_case("copy")
2569                    && matches!(
2570                        self.tokens.get(self.pos + 1),
2571                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2572                    ) =>
2573            {
2574                self.advance(); // COPY
2575                let table = self.expect_ident_like()?;
2576                let columns = if matches!(self.peek(), Token::LParen) {
2577                    self.advance();
2578                    let mut cols = alloc::vec![self.expect_ident_like()?];
2579                    while matches!(self.peek(), Token::Comma) {
2580                        self.advance();
2581                        cols.push(self.expect_ident_like()?);
2582                    }
2583                    if !matches!(self.peek(), Token::RParen) {
2584                        return Err(self.err(format!(
2585                            "expected ')' after COPY column list, got {:?}",
2586                            self.peek()
2587                        )));
2588                    }
2589                    self.advance();
2590                    Some(cols)
2591                } else {
2592                    None
2593                };
2594                // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2595                // endpoint. (FROM STDIN still rides the wire/import path —
2596                // its data arrives out of band.)
2597                if matches!(self.peek(), Token::From)
2598                    && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2599                {
2600                    self.advance(); // FROM
2601                    let Token::String(path) = self.advance() else {
2602                        unreachable!()
2603                    };
2604                    let options = self.parse_copy_to_options()?;
2605                    return Ok(Statement::CopyFromFile {
2606                        table,
2607                        columns,
2608                        path,
2609                        options,
2610                    });
2611                }
2612                if !matches!(self.peek(), Token::To) {
2613                    return Err(self.err(format!(
2614                        "COPY: only TO STDOUT is supported here (FROM stdin \
2615                         rides the import path); got {:?}",
2616                        self.peek()
2617                    )));
2618                }
2619                self.advance();
2620                if matches!(self.peek(), Token::String(_)) {
2621                    let Token::String(path) = self.advance() else { unreachable!() };
2622                    let options = self.parse_copy_to_options()?;
2623                    return Ok(Statement::CopyToFile {
2624                        table,
2625                        columns,
2626                        query: None,
2627                        path,
2628                        options,
2629                    });
2630                }
2631                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2632                    return Err(self.err(format!(
2633                        "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2634                        self.peek()
2635                    )));
2636                }
2637                self.advance();
2638                let options = self.parse_copy_to_options()?;
2639                Ok(Statement::CopyTo {
2640                    table,
2641                    columns,
2642                    query: None,
2643                    options,
2644                })
2645            }
2646            // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2647            // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2648            // result set is streamed in COPY format (PG's query form).
2649            Token::Ident(s)
2650                if s.eq_ignore_ascii_case("copy")
2651                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2652            {
2653                self.advance(); // COPY
2654                self.advance(); // (
2655                let query = self.parse_select_stmt()?;
2656                if !matches!(self.peek(), Token::RParen) {
2657                    return Err(self.err(format!(
2658                        "expected ')' after COPY query, got {:?}",
2659                        self.peek()
2660                    )));
2661                }
2662                self.advance(); // )
2663                if !matches!(self.peek(), Token::To) {
2664                    return Err(self.err(format!(
2665                        "COPY (query): only TO STDOUT is supported, got {:?}",
2666                        self.peek()
2667                    )));
2668                }
2669                self.advance();
2670                if matches!(self.peek(), Token::String(_)) {
2671                    let Token::String(path) = self.advance() else { unreachable!() };
2672                    let options = self.parse_copy_to_options()?;
2673                    return Ok(Statement::CopyToFile {
2674                        table: String::new(),
2675                        columns: None,
2676                        query: Some(alloc::boxed::Box::new(query)),
2677                        path,
2678                        options,
2679                    });
2680                }
2681                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2682                    return Err(self.err(format!(
2683                        "COPY (query): TO supports STDOUT only, got {:?}",
2684                        self.peek()
2685                    )));
2686                }
2687                self.advance();
2688                let options = self.parse_copy_to_options()?;
2689                Ok(Statement::CopyTo {
2690                    table: String::new(),
2691                    columns: None,
2692                    query: Some(alloc::boxed::Box::new(query)),
2693                    options,
2694                })
2695            }
2696            // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2697            // Shares the INSERT body; the replace flag lowers it
2698            // onto ON CONFLICT DO UPDATE with an empty assignment
2699            // list (engine: replace the whole row).
2700            Token::Ident(s)
2701                if s.eq_ignore_ascii_case("replace")
2702                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2703            {
2704                self.parse_insert_stmt(true)
2705            }
2706            Token::Begin => {
2707                self.advance();
2708                // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2709                // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2710                // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2711                // is consumed first, then the trailing modes — including the
2712                // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2713                // WORK/TRANSACTION). The explicit level, when present, rides the
2714                // statement so `exec_begin` applies it for this transaction.
2715                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2716                {
2717                    self.advance();
2718                }
2719                let iso = self.parse_isolation_level_clauses()?;
2720                Ok(Statement::Begin(iso))
2721            }
2722            // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2723            // for BEGIN. START is contextual in PG too; pattern-match
2724            // on the ident here. Iso clauses are parse-and-ignored,
2725            // same as BEGIN above.
2726            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2727                self.advance();
2728                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2729                {
2730                    return Err(self.err(alloc::format!(
2731                        "expected TRANSACTION after START, got {:?}",
2732                        self.peek()
2733                    )));
2734                }
2735                self.advance();
2736                let iso = self.parse_isolation_level_clauses()?;
2737                Ok(Statement::Begin(iso))
2738            }
2739            Token::Commit => {
2740                self.advance();
2741                // PG: `COMMIT [WORK | TRANSACTION]`.
2742                if let Token::Ident(w) = self.peek()
2743                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2744                {
2745                    self.advance();
2746                }
2747                Ok(Statement::Commit)
2748            }
2749            // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2750            // COMMIT synonym; pgbench's builtin tpcb-like script closes
2751            // every transaction with `END;` and the drop-in aborted on
2752            // it. Only reachable at statement start (CASE … END lives
2753            // inside expressions), so no ambiguity.
2754            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2755                self.advance();
2756                if let Token::Ident(w) = self.peek()
2757                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2758                {
2759                    self.advance();
2760                }
2761                Ok(Statement::Commit)
2762            }
2763            Token::Rollback => {
2764                self.advance();
2765                // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2766                // savepoint without ending the transaction. Bare
2767                // `ROLLBACK` drops the whole TX.
2768                if matches!(self.peek(), Token::To) {
2769                    self.advance();
2770                    if matches!(self.peek(), Token::Savepoint) {
2771                        self.advance();
2772                    }
2773                    let name = self.expect_ident_like()?;
2774                    Ok(Statement::RollbackToSavepoint(name))
2775                } else {
2776                    Ok(Statement::Rollback)
2777                }
2778            }
2779            Token::Savepoint => {
2780                self.advance();
2781                let name = self.expect_ident_like()?;
2782                Ok(Statement::Savepoint(name))
2783            }
2784            Token::Release => {
2785                self.advance();
2786                // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2787                // is optional in standard SQL.
2788                if matches!(self.peek(), Token::Savepoint) {
2789                    self.advance();
2790                }
2791                let name = self.expect_ident_like()?;
2792                Ok(Statement::ReleaseSavepoint(name))
2793            }
2794            Token::Show => {
2795                self.advance();
2796                // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2797                // v6.1.2 promoted TABLES to a reserved keyword (for
2798                // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2799                // arrives as `Token::Tables` rather than a bare ident.
2800                // USERS / COLUMNS remain bare idents.
2801                let target = match self.advance() {
2802                    Token::Tables => "tables".to_string(),
2803                    // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2804                    // keyword token; recognise it as the SHOW CREATE
2805                    // dispatch keyword too.
2806                    Token::Create => "create".to_string(),
2807                    // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2808                    // keyword too; let SHOW INDEX FROM parse.
2809                    Token::Index => "index".to_string(),
2810                    // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2811                    // reserved (used in aggregate function calls);
2812                    // recognise it here so the parser dispatches
2813                    // to ShowParameter("all") — the engine returns
2814                    // the curated parameter inventory.
2815                    Token::All => "all".to_string(),
2816                    // v7.38.18 (C12) — `SHOW COUNT(*) WARNINGS`, MySQL's
2817                    // spelling for the size of the diagnostics area.
2818                    // MySQL-dialect only: PostgreSQL 18.4 answers this
2819                    // phrase with `syntax error at or near "("`, and a
2820                    // PG session must keep getting exactly that rather
2821                    // than a message about an unknown parameter.
2822                    // `COUNT` arrives as a bare ident; the `(*)` and the
2823                    // trailing keyword are consumed here so the whole
2824                    // form reaches the engine as one parameter name.
2825                    Token::Ident(ref c)
2826                        if self.mysql_dialect
2827                            && c.eq_ignore_ascii_case("count")
2828                            && matches!(self.peek(), Token::LParen) =>
2829                    {
2830                        self.advance();
2831                        if matches!(self.peek(), Token::Star) {
2832                            self.advance();
2833                        }
2834                        if matches!(self.peek(), Token::RParen) {
2835                            self.advance();
2836                        }
2837                        match self.advance() {
2838                            Token::Ident(w) if w.eq_ignore_ascii_case("warnings") => {
2839                                return Ok(Statement::ShowParameter(
2840                                    "count(*) warnings".to_string(),
2841                                ));
2842                            }
2843                            other => {
2844                                return Err(self.err(format!(
2845                                    "expected WARNINGS after SHOW COUNT(*), got {other:?}"
2846                                )));
2847                            }
2848                        }
2849                    }
2850                    Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2851                    other => {
2852                        return Err(self.err(format!(
2853                            "expected SHOW target, got {other:?}"
2854                        )));
2855                    }
2856                };
2857                match target.as_str() {
2858                    "tables" => Ok(Statement::ShowTables),
2859                    "users" => Ok(Statement::ShowUsers),
2860                    // v7.38 轴 4 — `SHOW transaction_isolation`
2861                    // returns the currently-selected isolation level.
2862                    "transaction_isolation" => Ok(Statement::ShowParameter(
2863                        "transaction_isolation".to_string(),
2864                    )),
2865                    // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2866                    // TABLE <t>` returns a 2-column row: (Table,
2867                    // Create Table). mysqldump emits this for every
2868                    // table at scrape time; without it the dump
2869                    // round-trip stalls.
2870                    // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2871                    // FROM <t>` (also spelled `SHOW INDEX` and
2872                    // `SHOW KEYS`). admin / mysqldump probes use
2873                    // it to list per-table indexes.
2874                    "indexes" | "index" | "keys" => {
2875                        if !matches!(self.peek(), Token::From) {
2876                            return Err(self.err(format!(
2877                                "expected FROM after SHOW INDEXES, got {:?}",
2878                                self.peek()
2879                            )));
2880                        }
2881                        self.advance();
2882                        let table = self.expect_ident_like()?;
2883                        Ok(Statement::ShowIndexes(table))
2884                    }
2885                    // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2886                    // `SHOW VARIABLES`. Both return a 2-column row
2887                    // set listing server-side state; clients probe
2888                    // them at connect time.
2889                    "status" => Ok(Statement::ShowStatus),
2890                    "variables" => {
2891                        // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2892                        if matches!(self.peek(), Token::Like) {
2893                            self.advance();
2894                            let pat = match self.advance() {
2895                                Token::String(p) => p,
2896                                other => {
2897                                    return Err(self.err(format!(
2898                                        "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2899                                    )));
2900                                }
2901                            };
2902                            return Ok(Statement::ShowVariablesLike(pat));
2903                        }
2904                        Ok(Statement::ShowVariables)
2905                    }
2906                    // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2907                    "processlist" => Ok(Statement::ShowProcesslist),
2908                    "create" => {
2909                        // SHOW CREATE TABLE / VIEW / DATABASE — only
2910                        // TABLE is supported in v7.17.
2911                        let kind = match self.advance() {
2912                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2913                            Token::Table => "table".to_string(),
2914                            other => {
2915                                return Err(self.err(format!(
2916                                    "expected TABLE after SHOW CREATE, got {other:?}"
2917                                )));
2918                            }
2919                        };
2920                        if !kind.eq_ignore_ascii_case("table") {
2921                            return Err(self.err(format!(
2922                                "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2923                            )));
2924                        }
2925                        let name = self.expect_ident_like()?;
2926                        Ok(Statement::ShowCreateTable(name))
2927                    }
2928                    // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2929                    // (and `SHOW SCHEMAS` alias). The mysql client uses
2930                    // it to populate the database selector at connect
2931                    // time; without it `mysql -p` errors before the
2932                    // first user query.
2933                    "databases" | "schemas" => Ok(Statement::ShowDatabases),
2934                    // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2935                    // keyword on its own; it lands here as a bare
2936                    // ident. Returning all publications + their
2937                    // scope summary.
2938                    "publications" => Ok(Statement::ShowPublications),
2939                    // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2940                    "subscriptions" => Ok(Statement::ShowSubscriptions),
2941                    "columns" => {
2942                        if !matches!(self.peek(), Token::From) {
2943                            return Err(self.err(format!(
2944                                "expected FROM after SHOW COLUMNS, got {:?}",
2945                                self.peek()
2946                            )));
2947                        }
2948                        self.advance();
2949                        let table = self.expect_ident_like()?;
2950                        Ok(Statement::ShowColumns(table))
2951                    }
2952                    // v7.38 轴 4 surface — `SHOW <param>` for any
2953                    // remaining session / preset parameter name
2954                    // (server_version, search_path, client_encoding,
2955                    // …). The engine's ShowParameter handler does the
2956                    // dispatch; unrecognised names error there with
2957                    // a pointer to pg_settings, not at parse time —
2958                    // so a driver that issues `SHOW spam_setting`
2959                    // gets a clear runtime error instead of a
2960                    // confusing "unknown SHOW target".
2961                    other => {
2962                        // v7.38 (read01 P3.20) — a custom namespaced GUC
2963                        // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2964                        // consume the dotted tail so it round-trips with
2965                        // `SET app.foo` / `current_setting('app.foo')`.
2966                        let mut full = other.to_string();
2967                        while matches!(self.peek(), Token::Dot) {
2968                            self.advance();
2969                            let seg = self.expect_ident_like()?;
2970                            full.push('.');
2971                            full.push_str(&seg.to_ascii_lowercase());
2972                        }
2973                        Ok(Statement::ShowParameter(full))
2974                    }
2975                }
2976            }
2977            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2978            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2979            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2980            // arrived as a bare ident; tokenising it dedicatedly
2981            // keeps the dispatch tree small.
2982            Token::Drop => {
2983                self.advance();
2984                match self.peek() {
2985                    // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2986                    // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2987                    // around DROP ROLE cleanup. SPG has no role-owner
2988                    // model, so consume to boundary as a no-op.
2989                    Token::Ident(s) | Token::QuotedIdent(s)
2990                        if s.eq_ignore_ascii_case("owned") =>
2991                    {
2992                        // v7.39 (round 696) — still a no-op (SPG has no
2993                        // role-owner model), but the ROLE is carried out so
2994                        // the engine can refuse one that does not exist,
2995                        // which is what PG18 does.
2996                        self.advance();
2997                        if self.peek_is_by() {
2998                            self.advance();
2999                        }
3000                        let names = self.take_comma_separated_names();
3001                        self.consume_until_statement_boundary();
3002                        Ok(Statement::ValidateOnly {
3003                            kind: crate::ast::ValidateOnlyKind::RoleName,
3004                            names,
3005                        })
3006                    }
3007                    // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
3008                    // It drops only a TEMPORARY table, and name resolution
3009                    // already prefers the session's own, so the keyword is
3010                    // consumed and the ordinary DROP TABLE path runs.
3011                    Token::Ident(s) | Token::QuotedIdent(s)
3012                        if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
3013                    {
3014                        self.advance();
3015                        if !matches!(self.peek(), Token::Table) {
3016                            return Err(self.err(alloc::format!(
3017                                "expected TABLE after DROP TEMPORARY, got {:?}",
3018                                self.peek()
3019                            )));
3020                        }
3021                        self.parse_drop_table_after_keyword()
3022                    }
3023                    Token::Publication => {
3024                        self.advance();
3025                        // v7.39 (round 754, F31-B4) — the round-753
3026                        // audit probe tripped over the missing
3027                        // `IF EXISTS` here (syntax error).
3028                        let if_exists = self.consume_if_exists();
3029                        let name = self.expect_ident_or_string()?;
3030                        Ok(Statement::DropPublication { name, if_exists })
3031                    }
3032                    Token::Subscription => {
3033                        self.advance();
3034                        let if_exists = self.consume_if_exists();
3035                        let name = self.expect_ident_or_string()?;
3036                        Ok(Statement::DropSubscription { name, if_exists })
3037                    }
3038                    Token::Ident(s) | Token::QuotedIdent(s)
3039                        if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3040                    {
3041                        self.advance();
3042                        // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3043                        // login user IS a role in PG, and SPG's store holds
3044                        // both. `IF EXISTS` is accepted on either spelling.
3045                        let if_exists = self.consume_if_exists();
3046                        let name = self.expect_ident_or_string()?;
3047                        Ok(Statement::DropUser { name, if_exists })
3048                    }
3049                    // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3050                    // CREATE DATABASE has parsed since v7.14 and this did
3051                    // not, so `DROP DATABASE IF EXISTS x` — what every
3052                    // teardown script and pg_dumpall preamble opens with —
3053                    // came back as a syntax error, which IF EXISTS cannot
3054                    // soften. The name is carried so the engine can answer
3055                    // the way PG does; PG never lets this succeed on a
3056                    // single-database server, since the name is either
3057                    // unknown ("database … does not exist", or a notice
3058                    // under IF EXISTS) or the one you are connected to
3059                    // ("cannot drop the currently open database").
3060                    Token::Ident(s) | Token::QuotedIdent(s)
3061                        if s.eq_ignore_ascii_case("database") =>
3062                    {
3063                        self.advance();
3064                        let if_exists = self.consume_if_exists();
3065                        let name = self.expect_ident_or_string()?;
3066                        self.consume_until_statement_boundary();
3067                        Ok(Statement::DropDatabase { name, if_exists })
3068                    }
3069                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3070                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3071                        self.advance();
3072                        let if_exists = self.consume_if_exists();
3073                        let name = self.expect_ident_like()?;
3074                        // ON <table>
3075                        if !matches!(self.peek(), Token::On) {
3076                            return Err(self.err(alloc::format!(
3077                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3078                                self.peek()
3079                            )));
3080                        }
3081                        self.advance();
3082                        let table = self.expect_ident_like()?;
3083                        Ok(Statement::DropTrigger {
3084                            name,
3085                            table,
3086                            if_exists,
3087                        })
3088                    }
3089                    // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3090                    // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3091                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3092                        self.advance();
3093                        let if_exists = self.consume_if_exists();
3094                        let name = self.expect_ident_like()?;
3095                        if !matches!(self.peek(), Token::On) {
3096                            return Err(self.err(alloc::format!(
3097                                "expected ON <table> after DROP RULE {name:?}, got {:?}",
3098                                self.peek()
3099                            )));
3100                        }
3101                        self.advance();
3102                        let table = self.expect_ident_like()?;
3103                        // Optional CASCADE / RESTRICT — accepted, no effect.
3104                        self.consume_until_statement_boundary();
3105                        Ok(Statement::DropRule {
3106                            name,
3107                            table,
3108                            if_exists,
3109                        })
3110                    }
3111                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3112                    // v7.12.4 ignores any optional arg-list (signature-
3113                    // based overload disambiguation lands in v7.12.5+).
3114                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3115                        self.advance();
3116                        let if_exists = self.consume_if_exists();
3117                        let name = self.expect_ident_like()?;
3118                        // v7.39 (read01 round 62) — the argument list identifies
3119                        // WHICH overload to drop, so it is captured, not
3120                        // discarded. `DROP FUNCTION f` (no list) is legal when
3121                        // the name is unambiguous; the engine enforces that.
3122                        let args = if matches!(self.peek(), Token::LParen) {
3123                            Some(self.parse_function_signature_types()?)
3124                        } else {
3125                            None
3126                        };
3127                        // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3128                        // trailer, which `DROP TABLE` and `DROP INDEX` have
3129                        // accepted since v7.14 and this one refused outright.
3130                        // pg_dump writes it, so refusing was a parse error in
3131                        // the middle of a restore. SPG drops the function
3132                        // either way — it tracks no dependents to cascade to —
3133                        // which is the same reading the other two give it.
3134                        self.consume_drop_behaviour();
3135                        Ok(Statement::DropFunction {
3136                            name,
3137                            args,
3138                            if_exists,
3139                        })
3140                    }
3141                    // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3142                    // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3143                    // emit DROP TABLE IF EXISTS at the head of every
3144                    // CREATE TABLE block so re-importing a dump
3145                    // overwrites prior state. SPG accepts and removes
3146                    // matching tables; CASCADE/RESTRICT trailers
3147                    // accepted silently.
3148                    Token::Table => self.parse_drop_table_after_keyword(),
3149                    // v7.14.0 — DROP INDEX [IF EXISTS] name
3150                    // [CASCADE|RESTRICT]. PG / mysqldump emit this
3151                    // for partial-index renames and pgvector
3152                    // migrations. SPG removes the matching index;
3153                    // IF EXISTS makes the drop idempotent.
3154                    Token::Index => {
3155                        self.advance();
3156                        let if_exists = self.consume_if_exists();
3157                        let name = self.expect_ident_like()?;
3158                        if matches!(
3159                            self.peek(),
3160                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3161                                || s.eq_ignore_ascii_case("restrict")
3162                        ) {
3163                            self.advance();
3164                        }
3165                        Ok(Statement::DropIndex { name, if_exists })
3166                    }
3167                    // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3168                    // [CASCADE|RESTRICT]. SPG is single-database;
3169                    // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3170                    // name [, name…] [CASCADE | RESTRICT]. Real
3171                    // unregister (was silent no-op pre-v7.17).
3172                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3173                        self.advance();
3174                        let if_exists = self.consume_if_exists();
3175                        let mut names = vec![self.expect_ident_like()?];
3176                        while matches!(self.peek(), Token::Comma) {
3177                            self.advance();
3178                            names.push(self.expect_ident_like()?);
3179                        }
3180                        if matches!(
3181                            self.peek(),
3182                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3183                                || s.eq_ignore_ascii_case("restrict")
3184                        ) {
3185                            self.advance();
3186                        }
3187                        Ok(Statement::DropSchema { names, if_exists })
3188                    }
3189                    // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3190                    // name [, name…] [CASCADE|RESTRICT].
3191                    Token::Ident(s) | Token::QuotedIdent(s)
3192                        if s.eq_ignore_ascii_case("type") =>
3193                    {
3194                        self.advance();
3195                        let if_exists = self.consume_if_exists();
3196                        let mut names = vec![self.expect_ident_like()?];
3197                        while matches!(self.peek(), Token::Comma) {
3198                            self.advance();
3199                            names.push(self.expect_ident_like()?);
3200                        }
3201                        if matches!(
3202                            self.peek(),
3203                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3204                                || s.eq_ignore_ascii_case("restrict")
3205                        ) {
3206                            self.advance();
3207                        }
3208                        Ok(Statement::DropType { names, if_exists })
3209                    }
3210                    // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3211                    // name [, name…] [CASCADE|RESTRICT].
3212                    Token::Ident(s) | Token::QuotedIdent(s)
3213                        if s.eq_ignore_ascii_case("domain") =>
3214                    {
3215                        self.advance();
3216                        let if_exists = self.consume_if_exists();
3217                        let mut names = vec![self.expect_ident_like()?];
3218                        while matches!(self.peek(), Token::Comma) {
3219                            self.advance();
3220                            names.push(self.expect_ident_like()?);
3221                        }
3222                        if matches!(
3223                            self.peek(),
3224                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3225                                || s.eq_ignore_ascii_case("restrict")
3226                        ) {
3227                            self.advance();
3228                        }
3229                        Ok(Statement::DropDomain { names, if_exists })
3230                    }
3231                    // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3232                    // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3233                    Token::Ident(s) | Token::QuotedIdent(s)
3234                        if s.eq_ignore_ascii_case("materialized") =>
3235                    {
3236                        self.advance();
3237                        let nxt = self.peek().clone();
3238                        if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3239                        {
3240                            return Err(self.err(alloc::format!(
3241                                "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3242                            )));
3243                        }
3244                        self.advance();
3245                        let if_exists = self.consume_if_exists();
3246                        let mut names = vec![self.expect_ident_like()?];
3247                        while matches!(self.peek(), Token::Comma) {
3248                            self.advance();
3249                            names.push(self.expect_ident_like()?);
3250                        }
3251                        if matches!(
3252                            self.peek(),
3253                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3254                                || s.eq_ignore_ascii_case("restrict")
3255                        ) {
3256                            self.advance();
3257                        }
3258                        Ok(Statement::DropMaterializedView { names, if_exists })
3259                    }
3260                    // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3261                    // name [, name…] [CASCADE|RESTRICT].
3262                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3263                        self.advance();
3264                        let if_exists = self.consume_if_exists();
3265                        let mut names = vec![self.expect_ident_like()?];
3266                        while matches!(self.peek(), Token::Comma) {
3267                            self.advance();
3268                            names.push(self.expect_ident_like()?);
3269                        }
3270                        if matches!(
3271                            self.peek(),
3272                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3273                                || s.eq_ignore_ascii_case("restrict")
3274                        ) {
3275                            self.advance();
3276                        }
3277                        Ok(Statement::DropView { names, if_exists })
3278                    }
3279                    // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3280                    // [CASCADE|RESTRICT]. Real removal from catalog
3281                    // (was a silent no-op pre-v7.17).
3282                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3283                        self.advance();
3284                        let if_exists = self.consume_if_exists();
3285                        let mut names = vec![self.expect_ident_like()?];
3286                        while matches!(self.peek(), Token::Comma) {
3287                            self.advance();
3288                            names.push(self.expect_ident_like()?);
3289                        }
3290                        if matches!(
3291                            self.peek(),
3292                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3293                                || s.eq_ignore_ascii_case("restrict")
3294                        ) {
3295                            self.advance();
3296                        }
3297                        Ok(Statement::DropSequence { names, if_exists })
3298                    }
3299                    // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3300                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3301                        self.advance();
3302                        self.parse_drop_policy_after_keyword()
3303                    }
3304                    // v7.37.17 (17.6 siblings) — DROP <target> for
3305                    // targets SPG doesn't natively track. pg_dump
3306                    // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3307                    // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3308                    // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3309                    // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3310                    // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3311                    // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3312                    // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3313                    // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3314                    // etc. — accept + Empty-return so pg_dump tails
3315                    // load through. Materialized-view drop dispatches
3316                    // to the existing DropTable path when the token
3317                    // is Materialized-View-shaped (elsewhere in
3318                    // this parser).
3319                    Token::Ident(s) | Token::QuotedIdent(s)
3320                        if s.eq_ignore_ascii_case("text")
3321                            // The DROP dispatch matches on PEEK — `text` is
3322                            // not yet consumed, so SEARCH/CONFIGURATION sit
3323                            // at pos+1/pos+2 (the round-695 trap's mirror).
3324                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3325                            && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3326                    {
3327                        // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3328                        // validates the name; DICTIONARY / PARSER / TEMPLATE
3329                        // stay in the noise arm below.
3330                        self.advance(); // TEXT
3331                        self.advance(); // SEARCH
3332                        self.advance(); // CONFIGURATION
3333                        let if_exists = self.consume_if_exists();
3334                        let names = self.take_comma_separated_names();
3335                        self.consume_until_statement_boundary();
3336                        if if_exists {
3337                            return Ok(Statement::Empty);
3338                        }
3339                        Ok(Statement::ValidateOnly {
3340                            kind: crate::ast::ValidateOnlyKind::TsConfigName,
3341                            names,
3342                        })
3343                    }
3344                    Token::Ident(s) | Token::QuotedIdent(s)
3345                        if matches!(
3346                            s.to_ascii_lowercase().as_str(),
3347                            "type"
3348                                | "domain"
3349                                | "operator"
3350                                | "cast"
3351                                // `text` = TEXT SEARCH DICTIONARY / PARSER /
3352                                // TEMPLATE (CONFIGURATION intercepted above).
3353                                | "text"
3354                                | "materialized"
3355                                | "large"
3356                                | "role"
3357                                | "access"
3358                                | "procedure"
3359                                | "routine"
3360                        ) =>
3361                    {
3362                        self.consume_until_statement_boundary();
3363                        Ok(Statement::Empty)
3364                    }
3365                    // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3366                    // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3367                    // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3368                    // foreign-data warning family (round 706) so a
3369                    // CREATE→DROP sequence in a dump stays consistent.
3370                    Token::Ident(s) | Token::QuotedIdent(s)
3371                        if s.eq_ignore_ascii_case("server")
3372                            || s.eq_ignore_ascii_case("foreign") =>
3373                    {
3374                        self.advance();
3375                        self.consume_until_statement_boundary();
3376                        Ok(Statement::ValidateOnly {
3377                            kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3378                            names: Vec::new(),
3379                        })
3380                    }
3381                    Token::Ident(s) | Token::QuotedIdent(s)
3382                        if s.eq_ignore_ascii_case("collation")
3383                            || s.eq_ignore_ascii_case("tablespace") =>
3384                    {
3385                        let kind = if s.eq_ignore_ascii_case("collation") {
3386                            crate::ast::ValidateOnlyKind::CollationName
3387                        } else {
3388                            crate::ast::ValidateOnlyKind::TablespaceName
3389                        };
3390                        self.advance();
3391                        let if_exists = self.consume_if_exists();
3392                        let names = self.take_comma_separated_names();
3393                        self.consume_until_statement_boundary();
3394                        if if_exists {
3395                            return Ok(Statement::Empty);
3396                        }
3397                        Ok(Statement::ValidateOnly { kind, names })
3398                    }
3399                    Token::Ident(s) | Token::QuotedIdent(s)
3400                        if s.eq_ignore_ascii_case("event") =>
3401                    {
3402                        self.advance();
3403                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3404                        {
3405                            self.advance();
3406                        }
3407                        let if_exists = self.consume_if_exists();
3408                        let names = self.take_comma_separated_names();
3409                        self.consume_until_statement_boundary();
3410                        if if_exists {
3411                            return Ok(Statement::Empty);
3412                        }
3413                        Ok(Statement::ValidateOnly {
3414                            kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3415                            names,
3416                        })
3417                    }
3418                    // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3419                    // leave the noise list; see the ValidateOnly kinds.
3420                    Token::Ident(s) | Token::QuotedIdent(s)
3421                        if s.eq_ignore_ascii_case("conversion")
3422                            || s.eq_ignore_ascii_case("language")
3423                            // `DROP PROCEDURAL LANGUAGE` puts the modifier
3424                            // FIRST — the first draft looked for it after.
3425                            || s.eq_ignore_ascii_case("procedural") =>
3426                    {
3427                        let kind = if s.eq_ignore_ascii_case("conversion") {
3428                            crate::ast::ValidateOnlyKind::ConversionName
3429                        } else {
3430                            crate::ast::ValidateOnlyKind::LanguageName
3431                        };
3432                        self.advance();
3433                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3434                        {
3435                            self.advance();
3436                        }
3437                        let if_exists = self.consume_if_exists();
3438                        let names = self.take_comma_separated_names();
3439                        self.consume_until_statement_boundary();
3440                        if if_exists {
3441                            return Ok(Statement::Empty);
3442                        }
3443                        Ok(Statement::ValidateOnly { kind, names })
3444                    }
3445                    // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3446                    // name(argtypes)[, …]`. Parsed for real so the engine
3447                    // can answer as PG does; see Statement::DropAggregate.
3448                    Token::Ident(s) | Token::QuotedIdent(s)
3449                        if s.eq_ignore_ascii_case("aggregate") =>
3450                    {
3451                        self.advance();
3452                        let if_exists = self.consume_if_exists();
3453                        let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3454                        loop {
3455                            let name = self.expect_ident_like()?;
3456                            if !matches!(self.peek(), Token::LParen) {
3457                                return Err(self.err(alloc::format!(
3458                                    "expected argument list after DROP AGGREGATE {name}"
3459                                )));
3460                            }
3461                            self.advance();
3462                            let mut args: Vec<String> = Vec::new();
3463                            let mut star = false;
3464                            loop {
3465                                match self.peek().clone() {
3466                                    Token::RParen => {
3467                                        self.advance();
3468                                        break;
3469                                    }
3470                                    Token::Star => {
3471                                        self.advance();
3472                                        star = true;
3473                                    }
3474                                    Token::Comma => {
3475                                        self.advance();
3476                                    }
3477                                    _ => {
3478                                        // A type name may be multi-token
3479                                        // (`double precision`); glue idents
3480                                        // until , or ).
3481                                        let mut t = self.expect_ident_like()?;
3482                                        while let Token::Ident(nx) = self.peek() {
3483                                            let nx = nx.clone();
3484                                            self.advance();
3485                                            t.push(' ');
3486                                            t.push_str(&nx);
3487                                        }
3488                                        args.push(t);
3489                                    }
3490                                }
3491                            }
3492                            items.push((name, if star { None } else { Some(args) }));
3493                            if matches!(self.peek(), Token::Comma) {
3494                                self.advance();
3495                            } else {
3496                                break;
3497                            }
3498                        }
3499                        self.consume_until_statement_boundary();
3500                        Ok(Statement::DropAggregate { if_exists, items })
3501                    }
3502                    // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3503                    // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3504                    // installed; `IF EXISTS` is the spelling that says do
3505                    // not, and it keeps the no-op.
3506                    Token::Ident(s) | Token::QuotedIdent(s)
3507                        if s.eq_ignore_ascii_case("extension") =>
3508                    {
3509                        self.advance();
3510                        let if_exists = self.consume_if_exists();
3511                        let names = self.take_comma_separated_names();
3512                        self.consume_until_statement_boundary();
3513                        if if_exists {
3514                            return Ok(Statement::Empty);
3515                        }
3516                        Ok(Statement::ValidateOnly {
3517                            kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3518                            names,
3519                        })
3520                    }
3521                    Token::Ident(s) | Token::QuotedIdent(s)
3522                        if s.eq_ignore_ascii_case("statistics") =>
3523                    {
3524                        self.parse_drop_statistics_after_drop()
3525                    }
3526                    other => Err(self.err(format!(
3527                        "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3528                         SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3529                    ))),
3530                }
3531            }
3532            // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3533            // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3534            // and accepted before the view name. SPG materialised
3535            // views re-evaluate on read (always-fresh semantics), so
3536            // the CONCURRENTLY-vs-serial distinction has no runtime
3537            // effect — the refresh body does not block readers either
3538            // way. Same accept-and-no-op pattern as DETACH PARTITION
3539            // CONCURRENTLY (16.5).
3540            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3541                self.advance();
3542                let nxt = self.peek().clone();
3543                if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3544                {
3545                    return Err(self.err(alloc::format!(
3546                        "expected MATERIALIZED after REFRESH, got {nxt:?}"
3547                    )));
3548                }
3549                self.advance();
3550                let nxt2 = self.peek().clone();
3551                if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3552                {
3553                    return Err(self.err(alloc::format!(
3554                        "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3555                    )));
3556                }
3557                self.advance();
3558                // Optional CONCURRENTLY noise word — consumed without
3559                // changing semantics.
3560                if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3561                {
3562                    self.advance();
3563                }
3564                let name = self.expect_ident_like()?;
3565                let with_data = self.parse_optional_with_data(true)?;
3566                Ok(Statement::RefreshMaterializedView { name, with_data })
3567            }
3568            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3569                self.advance();
3570                self.parse_update_after_keyword()
3571            }
3572            // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3573            // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3574            // [CASCADE | RESTRICT]. Clears every row from each named
3575            // table. Parses at the top level; the engine dispatcher
3576            // walks Statement::Truncate.
3577            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3578                self.advance();
3579                // Optional TABLE noise word — PG accepts both the reserved
3580                // token and the bare identifier spelling.
3581                if matches!(self.peek(), Token::Table)
3582                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3583                {
3584                    self.advance();
3585                }
3586                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3587                // not absorbed. The lookahead keeps a table genuinely
3588                // called `only` working: the keyword is a keyword only
3589                // when a name follows it.
3590                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3591                    if s.eq_ignore_ascii_case("only"))
3592                    && matches!(
3593                        self.tokens.get(self.pos + 1),
3594                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3595                    );
3596                if only {
3597                    self.advance();
3598                }
3599                // Table names (comma-separated).
3600                let mut tables = Vec::new();
3601                loop {
3602                    tables.push(self.expect_ident_like()?);
3603                    if matches!(self.peek(), Token::Comma) {
3604                        self.advance();
3605                        continue;
3606                    }
3607                    break;
3608                }
3609                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3610                let mut restart_identity = false;
3611                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3612                {
3613                    self.advance();
3614                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3615                    {
3616                        self.advance();
3617                        restart_identity = true;
3618                    }
3619                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3620                {
3621                    self.advance();
3622                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3623                    {
3624                        self.advance();
3625                    }
3626                }
3627                // Optional CASCADE / RESTRICT.
3628                let mut cascade = false;
3629                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3630                {
3631                    self.advance();
3632                    cascade = true;
3633                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3634                {
3635                    self.advance();
3636                }
3637                Ok(Statement::Truncate {
3638                    tables,
3639                    restart_identity,
3640                    cascade,
3641                    only,
3642                })
3643            }
3644            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3645            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3646            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3647            // rows change so the index tree is always up-to-date;
3648            // REINDEX is a strict no-op. Accept the whole statement
3649            // shape to boundary for pg_dump round-trip compatibility.
3650            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3651                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3652                // index bloat to rebuild, so the work stays a no-op, but PG
3653                // validates what it was pointed at and this swallowed the
3654                // name at parse time — `REINDEX TABLE typo` reported
3655                // success. Measured on PG18: INDEX / TABLE name a relation,
3656                // SCHEMA a schema, SYSTEM nothing.
3657                self.advance();
3658                self.parse_reindex_tail()
3659            }
3660            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3661            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3662            // SPG has no MVCC bloat today (Phase D visibility map
3663            // queues with v7.38); the freezer collapses hot-tier
3664            // rows into cold segments automatically. VACUUM is a
3665            // no-op — pg_dump maintenance scripts and Discourse's
3666            // periodic-maintenance path both emit it.
3667            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3668            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3669            // actual bloat, so the pre-MVCC accept-and-ignore posture
3670            // became a silent no-op on a customer's manual reclaim.
3671            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3672            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3673            // ANALYZE is captured, the optional table name is captured.
3674            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3675                self.advance();
3676                // Parenthesised option list: absorb it.
3677                if matches!(self.peek(), Token::LParen) {
3678                    let mut depth = 0usize;
3679                    loop {
3680                        match self.advance() {
3681                            Token::LParen => depth += 1,
3682                            Token::RParen => {
3683                                depth -= 1;
3684                                if depth == 0 {
3685                                    break;
3686                                }
3687                            }
3688                            Token::Eof => break,
3689                            _ => {}
3690                        }
3691                    }
3692                }
3693                let mut analyze = false;
3694                let mut table: Option<String> = None;
3695                loop {
3696                    match self.peek() {
3697                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3698                        // an identifier, so the loop below broke out on it and
3699                        // dropped the table name: `VACUUM FULL nosuch` was
3700                        // accepted where `VACUUM nosuch` was refused.
3701                        Token::Full => {
3702                            self.advance();
3703                        }
3704                        Token::Ident(w) | Token::QuotedIdent(w) => {
3705                            let wl = w.to_ascii_lowercase();
3706                            match wl.as_str() {
3707                                "full" | "freeze" | "verbose" => {
3708                                    self.advance();
3709                                }
3710                                "analyze" | "analyse" => {
3711                                    analyze = true;
3712                                    self.advance();
3713                                }
3714                                _ => {
3715                                    table = Some(self.expect_ident_like()?);
3716                                    break;
3717                                }
3718                            }
3719                        }
3720                        _ => break,
3721                    }
3722                }
3723                // Optional trailing column list / anything else to the
3724                // statement boundary (PG accepts per-column ANALYZE).
3725                self.consume_until_statement_boundary();
3726                Ok(Statement::Vacuum { table, analyze })
3727            }
3728            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3729            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3730            // <index>. PG stores rows in physical order matching
3731            // an index; SPG's hot-tier is append-only + cold-tier
3732            // is segment-frozen, so clustering has no persistent
3733            // effect. Accept-and-no-op for pg_dump compat.
3734            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3735                // v7.39 (round 535) — same as REINDEX above: the relation is
3736                // carried so the engine can refuse one that does not exist.
3737                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3738                self.advance();
3739                self.parse_cluster_tail()
3740            }
3741            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3742            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3743            // optional string payload; UNLISTEN takes a channel or `*`.
3744            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3745                self.advance();
3746                let ch = match self.advance() {
3747                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3748                    other => {
3749                        return Err(self.err(format!(
3750                            "expected channel name after LISTEN, got {other:?}"
3751                        )));
3752                    }
3753                };
3754                Ok(Statement::Listen(ch))
3755            }
3756            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3757                self.advance();
3758                let channel = match self.advance() {
3759                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3760                    other => {
3761                        return Err(self.err(format!(
3762                            "expected channel name after NOTIFY, got {other:?}"
3763                        )));
3764                    }
3765                };
3766                let payload = if matches!(self.peek(), Token::Comma) {
3767                    self.advance();
3768                    match self.advance() {
3769                        Token::String(p) => Some(p),
3770                        other => {
3771                            return Err(self.err(format!(
3772                                "expected string payload after NOTIFY <channel>, got {other:?}"
3773                            )));
3774                        }
3775                    }
3776                } else {
3777                    None
3778                };
3779                Ok(Statement::Notify { channel, payload })
3780            }
3781            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3782                self.advance();
3783                match self.advance() {
3784                    Token::Star => Ok(Statement::Unlisten(None)),
3785                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3786                    other => Err(self.err(format!(
3787                        "expected channel name or * after UNLISTEN, got {other:?}"
3788                    ))),
3789                }
3790            }
3791            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3792            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3793            // process-wide write lock today; explicit LOCK has no
3794            // effect. Accept-and-no-op for pg_dump / migration
3795            // compat.
3796            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3797                self.advance();
3798                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3799                // engine holds a process-wide write lock), but the TABLE
3800                // NAME is now carried out so the engine can refuse one that
3801                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3802                // READ|WRITE` is a different statement with the same first
3803                // word; it keeps the old no-op, because a MySQL dump's
3804                // bracket names tables it is about to create.
3805                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3806                    if k.eq_ignore_ascii_case("tables"));
3807                if mysql_tables {
3808                    self.consume_until_statement_boundary();
3809                    return Ok(Statement::Empty);
3810                }
3811                if matches!(self.peek(), Token::Table) {
3812                    self.advance();
3813                }
3814                let names = self.take_comma_separated_names();
3815                self.consume_until_statement_boundary();
3816                Ok(Statement::ValidateOnly {
3817                    kind: crate::ast::ValidateOnlyKind::LockTable,
3818                    names,
3819                })
3820            }
3821            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3822            // durability marker + snapshot in PG. SPG has WAL
3823            // checkpointing on a byte / time schedule (v7.37.10
3824            // 60s / 4 MiB defaults). The bare statement parses to
3825            // `Statement::Empty` here (the no_std engine owns no
3826            // WAL / snapshot); v7.37 Epic Du wires the HOST
3827            // (embedded `Database::execute_buffered`, via
3828            // `sql_is_checkpoint`) to force an immediate synchronous
3829            // checkpoint through `Database::checkpoint` — a real
3830            // durability barrier, matching PG.
3831            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3832                self.advance();
3833                self.consume_until_statement_boundary();
3834                Ok(Statement::Empty)
3835            }
3836            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3837                self.advance();
3838                self.parse_delete_after_keyword()
3839            }
3840            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3841            // ALTER is not a reserved keyword in the lexer — handled
3842            // as a bare ident here.
3843            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3844                self.advance();
3845                self.parse_alter_after_keyword()
3846            }
3847            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3848            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3849            // additions needed.
3850            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3851                self.advance();
3852                self.parse_wait_after_keyword()
3853            }
3854            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3855            // Bare ANALYZE → analyse every user table; ANALYZE
3856            // <name> → re-stats one. The argument is an optional
3857            // ident (or quoted ident); anything else is a parse
3858            // error.
3859            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3860            // `WHERE` filter (carved out per V6_7_DESIGN.md
3861            // STABILITY). Lex order: identifier "compact" → "cold"
3862            // → "segments". Anything else after `COMPACT` is a
3863            // parse error.
3864            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3865                self.advance();
3866                let next = self.peek().clone();
3867                let cold = match next {
3868                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3869                    _ => {
3870                        return Err(
3871                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3872                        );
3873                    }
3874                };
3875                if !cold.eq_ignore_ascii_case("cold") {
3876                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3877                }
3878                self.advance();
3879                let next = self.peek().clone();
3880                let segments = match next {
3881                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3882                    _ => {
3883                        return Err(self.err(format!(
3884                            "expected SEGMENTS after COMPACT COLD, got {:?}",
3885                            self.peek()
3886                        )));
3887                    }
3888                };
3889                if !segments.eq_ignore_ascii_case("segments") {
3890                    return Err(self.err(format!(
3891                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3892                    )));
3893                }
3894                self.advance();
3895                Ok(Statement::CompactColdSegments)
3896            }
3897            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3898            // Parsed as a case-insensitive identifier since MERGE
3899            // isn't a reserved lexer keyword (collides with the
3900            // mysqldump `ALGORITHM = MERGE` view clause if it
3901            // were); the inner parser drives the rest of the
3902            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3903            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3904                self.advance();
3905                self.parse_merge_after_keyword()
3906            }
3907            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3908                self.advance();
3909                let target = match self.peek() {
3910                    Token::Eof | Token::Semicolon => None,
3911                    Token::Ident(_) | Token::QuotedIdent(_) => {
3912                        Some(self.expect_ident_like()?)
3913                    }
3914                    other => {
3915                        return Err(self.err(format!(
3916                            "expected table name or end of statement after ANALYZE, got {other:?}"
3917                        )));
3918                    }
3919                };
3920                // v7.39 (round 776, F31 J7) — the per-column form
3921                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3922                // here while the VACUUM arm already consumed it; SPG
3923                // analyzes whole tables, so the list parses and is
3924                // accepted like the VACUUM path's.
3925                if target.is_some() && matches!(self.peek(), Token::LParen) {
3926                    self.advance();
3927                    loop {
3928                        let _ = self.expect_ident_like()?;
3929                        match self.peek() {
3930                            Token::Comma => {
3931                                self.advance();
3932                            }
3933                            Token::RParen => {
3934                                self.advance();
3935                                break;
3936                            }
3937                            other => {
3938                                return Err(self.err(format!(
3939                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
3940                                )));
3941                            }
3942                        }
3943                    }
3944                }
3945                Ok(Statement::Analyze(target))
3946            }
3947            // v7.12.1 — `SET <name> [TO|=] <value>`. The
3948            // `default_text_search_config` parameter is consumed
3949            // by the FTS function dispatcher; other parameter
3950            // names are recorded but treated as a no-op so PG
3951            // dump output loads.
3952            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3953                self.advance();
3954                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3955                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3956                // …` which the SessionVar path handles). `LOCAL` is the only
3957                // one that changes semantics — it scopes the change to the
3958                // current transaction — so capture it; SESSION / GLOBAL are
3959                // accepted and treated as the default session scope.
3960                let mut set_local = false;
3961                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3962                    let q = s.to_ascii_lowercase();
3963                    if q == "local" || q == "session" || q == "global" {
3964                        set_local = q == "local";
3965                        self.advance();
3966                    }
3967                }
3968                // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
3969                // { DEFAULT | <role> }`. pg_dump's ACL section switches
3970                // to the object owner with it. SPG maps it onto the
3971                // session-role machinery (recorded delta RD-10: PG moves
3972                // session_user too; SPG moves the effective role).
3973                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3974                    if s.eq_ignore_ascii_case("authorization"))
3975                {
3976                    self.advance(); // AUTHORIZATION
3977                    let role = match self.peek().clone() {
3978                        Token::Default => {
3979                            self.advance();
3980                            None
3981                        }
3982                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3983                            self.advance();
3984                            Some(s)
3985                        }
3986                        _ => None,
3987                    };
3988                    return Ok(Statement::SetRole(role));
3989                }
3990                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3991                // <collation>]` — change the connection client
3992                // charset. SPG stores UTF-8 always and orders
3993                // bytewise; accept as a no-op.
3994                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3995                {
3996                    self.advance();
3997                    // Charset ident-or-string.
3998                    if matches!(
3999                        self.peek(),
4000                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4001                    ) {
4002                        self.advance();
4003                    }
4004                    // Optional `COLLATE <name>`.
4005                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4006                    {
4007                        self.advance();
4008                        if matches!(
4009                            self.peek(),
4010                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4011                        ) {
4012                            self.advance();
4013                        }
4014                    }
4015                    return Ok(Statement::Empty);
4016                }
4017                // v7.37.17 (17.6 sibling) — PG `SET ROLE
4018                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4019                // uses this to switch to the object owner before
4020                // recreating tables. SPG has no role system so this
4021                // is a no-op.
4022                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4023                {
4024                    self.advance(); // ROLE
4025                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4026                    // reset to the login identity; a name / string sets the
4027                    // effective role that drives current_user + RLS.
4028                    let role = match self.peek().clone() {
4029                        Token::Default => {
4030                            self.advance();
4031                            None
4032                        }
4033                        Token::Ident(s) | Token::QuotedIdent(s)
4034                            if s.eq_ignore_ascii_case("none") =>
4035                        {
4036                            self.advance();
4037                            None
4038                        }
4039                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4040                            self.advance();
4041                            Some(s)
4042                        }
4043                        _ => None,
4044                    };
4045                    return Ok(Statement::SetRole(role));
4046                }
4047                // v7.37.17 (17.6 sibling) — PG `SET SESSION
4048                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4049                // ISO SQL surface). pg_dump prepends this to fix
4050                // the isolation level for the restore session. SPG
4051                // defaults to READ COMMITTED and doesn't yet honor
4052                // session-set isolation across statements — accept
4053                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4054                // per-tx form is handled elsewhere.
4055                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4056                {
4057                    self.advance(); // CHARACTERISTICS
4058                    self.consume_until_statement_boundary();
4059                    return Ok(Statement::Empty);
4060                }
4061                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4062                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4063                // pg_dump emits this to control the deferrability of
4064                // FK / UNIQUE constraints across a bulk restore. SPG
4065                // has no deferrable-constraint machinery today; the
4066                // FK checker is strict-immediate. Accept-and-no-op
4067                // for pg_dump round-trip compatibility.
4068                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4069                {
4070                    self.advance(); // CONSTRAINTS
4071                    // v7.39 (round 288) — no longer a no-op: the trailing
4072                    // DEFERRED / IMMEDIATE sets the transaction's timing.
4073                    // v7.39 (round 308, V29) — and the names are kept.
4074                    // They used to be skipped over on the way to the
4075                    // DEFERRED keyword, so a named form silently behaved
4076                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4077                    // every deferrable constraint in the transaction.
4078                    let mut names: alloc::vec::Vec<alloc::string::String> =
4079                        alloc::vec::Vec::new();
4080                    if matches!(self.peek(), Token::All) {
4081                        self.advance();
4082                    } else {
4083                        loop {
4084                            let mut n = self.expect_ident_like()?;
4085                            // A schema-qualified name (`public.fk_a`)
4086                            // identifies the same constraint; PG resolves
4087                            // it by the trailing segment.
4088                            while matches!(self.peek(), Token::Dot) {
4089                                self.advance();
4090                                n = self.expect_ident_like()?;
4091                            }
4092                            names.push(n);
4093                            if matches!(self.peek(), Token::Comma) {
4094                                self.advance();
4095                            } else {
4096                                break;
4097                            }
4098                        }
4099                    }
4100                    let deferred = match self.peek() {
4101                        Token::Ident(s) | Token::QuotedIdent(s)
4102                            if s.eq_ignore_ascii_case("deferred") =>
4103                        {
4104                            true
4105                        }
4106                        Token::Ident(s) | Token::QuotedIdent(s)
4107                            if s.eq_ignore_ascii_case("immediate") =>
4108                        {
4109                            false
4110                        }
4111                        other => {
4112                            return Err(self.err(alloc::format!(
4113                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4114                            )));
4115                        }
4116                    };
4117                    self.advance();
4118                    return Ok(Statement::SetConstraints { names, deferred });
4119                }
4120                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4121                // { DEFAULT | '<role>' | <ident> }` (mailrs
4122                // round-10 A.1). pg_dump preamble emits the
4123                // `DEFAULT` form to reset session authorization.
4124                //
4125                // v7.39 (round 697) — this said "SPG has no role system so
4126                // this is a strict no-op". SPG has had one since round 58;
4127                // the comment outlived it, and with it the reason a name
4128                // that is not a role was accepted here. It still switches
4129                // no authorization — what it does now is refuse a role
4130                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4131                // AUTHORIZATION` (handled by the RESET parser
4132                // elsewhere). Reference:
4133                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4134                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4135                {
4136                    self.advance(); // AUTHORIZATION
4137                    match self.peek().clone() {
4138                        Token::Default => {
4139                            self.advance();
4140                        }
4141                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4142                            self.advance();
4143                            return Ok(Statement::ValidateOnly {
4144                                kind: crate::ast::ValidateOnlyKind::RoleName,
4145                                names: alloc::vec![r],
4146                            });
4147                        }
4148                        other => {
4149                            return Err(self.err(alloc::format!(
4150                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4151                            )));
4152                        }
4153                    }
4154                    return Ok(Statement::Empty);
4155                }
4156                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4157                // ISOLATION LEVEL { READ COMMITTED | READ
4158                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4159                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4160                // PG-standard surface. v7.37.8 accepts the syntax
4161                // and tracks the selected level on
4162                // `Engine::current_isolation_level()`; the actual
4163                // MVCC / SSI semantics implementation lands in
4164                // the 轴 4 isolation framework (separate train).
4165                // PG itself maps READ UNCOMMITTED to READ COMMITTED
4166                // internally; SPG behaves the same (effectively
4167                // READ COMMITTED at every level today).
4168                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4169                {
4170                    self.advance(); // TRANSACTION
4171                    let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
4172                    return Ok(Statement::SetTransaction { isolation: level });
4173                }
4174                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4175                // alias — same accept-as-no-op as SET NAMES.
4176                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4177                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4178                {
4179                    self.advance(); // CHARACTER
4180                    self.advance(); // SET
4181                    if matches!(
4182                        self.peek(),
4183                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4184                    ) {
4185                        self.advance();
4186                    }
4187                    return Ok(Statement::Empty);
4188                }
4189                // v7.39 (GUC) — PG spells the timezone GUC as two
4190                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4191                // where <value> is a string/ident or the LOCAL /
4192                // DEFAULT keyword (both mean "back to the default").
4193                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4194                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4195                {
4196                    self.advance(); // TIME
4197                    self.advance(); // ZONE
4198                    let value = match self.peek().clone() {
4199                        Token::Ident(s)
4200                            if s.eq_ignore_ascii_case("local")
4201                                || s.eq_ignore_ascii_case("default") =>
4202                        {
4203                            self.advance();
4204                            crate::ast::SetValue::Default
4205                        }
4206                        Token::Default => {
4207                            self.advance();
4208                            crate::ast::SetValue::Default
4209                        }
4210                        _ => self.parse_set_value()?,
4211                    };
4212                    return Ok(Statement::SetParameter {
4213                        name: "timezone".into(),
4214                        value,
4215                        local: set_local,
4216                    });
4217                }
4218                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4219                // MySQL USER-variable assignment: its own per-session
4220                // namespace, an arbitrary expression on the right, and `:=`
4221                // as a second spelling of `=`. It used to fall into the
4222                // session-PARAMETER list below, whose values are literals and
4223                // whose store nothing reads back under a `@` name — so the
4224                // assignment reported success and vanished.
4225                //
4226                // A `@@`-prefixed LHS is a real engine setting and keeps the
4227                // old path.
4228                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4229                    return self.parse_set_user_vars();
4230                }
4231                // v7.14.0 — multi-assignment form
4232                // `SET a = 1, b = 2, …`. Single-assignment is the
4233                // 1-element case. Each LHS may be a regular ident
4234                // or a SessionVar (`@VAR` / `@@VAR`).
4235                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4236                loop {
4237                    let lhs = match self.peek().clone() {
4238                        Token::SessionVar(s) => {
4239                            self.advance();
4240                            s
4241                        }
4242                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4243                        other => {
4244                            return Err(self.err(format!(
4245                                "expected parameter name after SET, got {other:?}"
4246                            )));
4247                        }
4248                    };
4249                    // Accept either `=` or the bare `TO` keyword.
4250                    match self.peek() {
4251                        Token::Eq => {
4252                            self.advance();
4253                        }
4254                        Token::To => {
4255                            self.advance();
4256                        }
4257                        other => {
4258                            return Err(self.err(format!(
4259                                "expected `=` or TO after SET {lhs}, got {other:?}"
4260                            )));
4261                        }
4262                    }
4263                    let mut value = self.parse_set_value()?;
4264                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4265                    // `, name TO` continues a MySQL-style multi-assign,
4266                    // anything else is a PG list VALUE
4267                    // (`SET search_path = myschema, public`) folded into
4268                    // one comma-joined string.
4269                    while matches!(self.peek(), Token::Comma) {
4270                        let is_assign = matches!(
4271                            self.tokens.get(self.pos + 1),
4272                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4273                        ) && matches!(
4274                            self.tokens.get(self.pos + 2),
4275                            Some(Token::Eq | Token::To)
4276                        );
4277                        if is_assign {
4278                            break;
4279                        }
4280                        self.advance(); // comma
4281                        let next = self.parse_set_value()?;
4282                        let joined = alloc::format!(
4283                            "{}, {}",
4284                            set_value_text(&value),
4285                            set_value_text(&next)
4286                        );
4287                        value = crate::ast::SetValue::String(joined);
4288                    }
4289                    pairs.push((lhs, value));
4290                    if matches!(self.peek(), Token::Comma) {
4291                        self.advance();
4292                        continue;
4293                    }
4294                    break;
4295                }
4296                if pairs.len() == 1 {
4297                    let (name, value) = pairs.into_iter().next().unwrap();
4298                    Ok(Statement::SetParameter {
4299                        name,
4300                        value,
4301                        local: set_local,
4302                    })
4303                } else {
4304                    Ok(Statement::SetParameterList(pairs))
4305                }
4306            }
4307            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4308            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4309                self.advance();
4310                match self.peek().clone() {
4311                    Token::All => {
4312                        self.advance();
4313                        Ok(Statement::ResetParameter(None))
4314                    }
4315                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4316                        self.advance();
4317                        Ok(Statement::ResetParameter(None))
4318                    }
4319                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4320                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4321                        self.advance();
4322                        Ok(Statement::SetRole(None))
4323                    }
4324                    // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4325                    // (pg_dump's return from the owner switch).
4326                    Token::Ident(s) | Token::QuotedIdent(s)
4327                        if s.eq_ignore_ascii_case("session")
4328                            && matches!(
4329                                self.tokens.get(self.pos + 1),
4330                                Some(Token::Ident(a) | Token::QuotedIdent(a))
4331                                    if a.eq_ignore_ascii_case("authorization")
4332                            ) =>
4333                    {
4334                        self.advance(); // SESSION
4335                        self.advance(); // AUTHORIZATION
4336                        Ok(Statement::SetRole(None))
4337                    }
4338                    _ => {
4339                        let name = self.parse_set_param_name()?;
4340                        Ok(Statement::ResetParameter(Some(name)))
4341                    }
4342                }
4343            }
4344            // v7.39 (round 218) — server-side cursors.
4345            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4346            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4347            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4348            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4349                self.advance();
4350                match self.peek().clone() {
4351                    Token::All => {
4352                        self.advance();
4353                        Ok(Statement::CloseCursor { name: None })
4354                    }
4355                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4356                        self.advance();
4357                        Ok(Statement::CloseCursor { name: None })
4358                    }
4359                    Token::Ident(n) | Token::QuotedIdent(n) => {
4360                        self.advance();
4361                        Ok(Statement::CloseCursor { name: Some(n) })
4362                    }
4363                    other => Err(self.err(format!(
4364                        "expected cursor name or ALL after CLOSE, got {other:?}"
4365                    ))),
4366                }
4367            }
4368            other => Err(self.err(format!(
4369                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4370                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4371            ))),
4372        }
4373    }
4374
4375    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4376    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4377    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4378    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4379    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4380        self.advance(); // DECLARE
4381        let name = match self.advance() {
4382            Token::Ident(n) | Token::QuotedIdent(n) => n,
4383            other => {
4384                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4385            }
4386        };
4387        let mut scroll: Option<bool> = None;
4388        loop {
4389            match self.peek() {
4390                Token::Ident(s)
4391                    if s.eq_ignore_ascii_case("binary")
4392                        || s.eq_ignore_ascii_case("insensitive")
4393                        || s.eq_ignore_ascii_case("asensitive") =>
4394                {
4395                    self.advance();
4396                }
4397                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4398                    self.advance();
4399                    scroll = Some(true);
4400                }
4401                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4402                {
4403                    self.advance(); // NO
4404                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4405                        return Err(self.err(format!(
4406                            "expected SCROLL after NO in DECLARE, got {:?}",
4407                            self.peek()
4408                        )));
4409                    }
4410                    self.advance();
4411                    scroll = Some(false);
4412                }
4413                _ => break,
4414            }
4415        }
4416        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4417            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4418        }
4419        self.advance();
4420        let mut hold = false;
4421        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4422            self.advance();
4423            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4424                return Err(self.err(format!(
4425                    "expected HOLD after WITH in DECLARE, got {:?}",
4426                    self.peek()
4427                )));
4428            }
4429            self.advance();
4430            hold = true;
4431        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4432            self.advance();
4433            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4434                return Err(self.err(format!(
4435                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4436                    self.peek()
4437                )));
4438            }
4439            self.advance();
4440        }
4441        if !matches!(self.peek(), Token::For) {
4442            return Err(self.err(format!(
4443                "expected FOR before the cursor query, got {:?}",
4444                self.peek()
4445            )));
4446        }
4447        self.advance();
4448        let query = self.parse_one_statement()?;
4449        Ok(Statement::DeclareCursor {
4450            name,
4451            scroll,
4452            hold,
4453            query: alloc::boxed::Box::new(query),
4454        })
4455    }
4456
4457    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4458    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4459    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4460    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4461        use crate::ast::CursorDirection as D;
4462        self.advance(); // FETCH / MOVE
4463        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4464            let neg = if matches!(this.peek(), Token::Minus) {
4465                this.advance();
4466                true
4467            } else {
4468                false
4469            };
4470            match this.advance() {
4471                Token::Integer(v) => Ok(if neg { -v } else { v }),
4472                other => Err(this.err(format!("expected count, got {other:?}"))),
4473            }
4474        };
4475        let direction = match self.peek().clone() {
4476            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4477                self.advance();
4478                D::Next
4479            }
4480            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4481                self.advance();
4482                D::Prior
4483            }
4484            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4485                self.advance();
4486                D::First
4487            }
4488            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4489                self.advance();
4490                D::Last
4491            }
4492            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4493                self.advance();
4494                D::Absolute(signed_count(self)?)
4495            }
4496            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4497                self.advance();
4498                D::Relative(signed_count(self)?)
4499            }
4500            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4501                self.advance();
4502                match self.peek().clone() {
4503                    Token::All => {
4504                        self.advance();
4505                        D::All
4506                    }
4507                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4508                        self.advance();
4509                        D::All
4510                    }
4511                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4512                    _ => D::Next, // bare FORWARD = FORWARD 1
4513                }
4514            }
4515            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4516                self.advance();
4517                match self.peek().clone() {
4518                    Token::All => {
4519                        self.advance();
4520                        D::BackwardAll
4521                    }
4522                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4523                        self.advance();
4524                        D::BackwardAll
4525                    }
4526                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4527                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4528                }
4529            }
4530            Token::All => {
4531                self.advance();
4532                D::All
4533            }
4534            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4535                self.advance();
4536                D::All
4537            }
4538            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4539            // Bare `FETCH <name>` — direction defaults to NEXT.
4540            _ => D::Next,
4541        };
4542        // Optional FROM / IN.
4543        if matches!(self.peek(), Token::From)
4544            || matches!(self.peek(), Token::In)
4545            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4546        {
4547            self.advance();
4548        }
4549        let name = match self.advance() {
4550            Token::Ident(n) | Token::QuotedIdent(n) => n,
4551            other => {
4552                return Err(self.err(format!("expected cursor name, got {other:?}")));
4553            }
4554        };
4555        Ok(if is_move {
4556            Statement::MoveCursor { name, direction }
4557        } else {
4558            Statement::FetchCursor { name, direction }
4559        })
4560    }
4561
4562    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4563    /// [(kind, …)] ON <col>, … FROM <table>`.
4564    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4565        self.advance(); // STATISTICS
4566        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4567        let mut if_not_exists = false;
4568        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4569            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4570        {
4571            self.advance();
4572            self.advance();
4573            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4574                self.advance();
4575                if_not_exists = true;
4576            }
4577        }
4578        let name = self.expect_ident_like()?;
4579        let mut kinds = Vec::new();
4580        if matches!(self.peek(), Token::LParen) {
4581            self.advance();
4582            loop {
4583                let k = self.expect_ident_like()?;
4584                // PG stores the single letters; accept the spelled-out
4585                // names the SQL uses and record what PG records.
4586                kinds.push(match k.to_ascii_lowercase().as_str() {
4587                    "ndistinct" => String::from("d"),
4588                    "dependencies" => String::from("f"),
4589                    "mcv" => String::from("m"),
4590                    other => {
4591                        return Err(
4592                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4593                        );
4594                    }
4595                });
4596                match self.advance() {
4597                    Token::Comma => {}
4598                    Token::RParen => break,
4599                    other => {
4600                        return Err(self.err(alloc::format!(
4601                            "expected ',' or ')' in statistics kind list, got {other:?}"
4602                        )));
4603                    }
4604                }
4605            }
4606        }
4607        if !matches!(self.peek(), Token::On) {
4608            return Err(self.err(alloc::format!(
4609                "expected ON in CREATE STATISTICS, got {:?}",
4610                self.peek()
4611            )));
4612        }
4613        self.advance();
4614        let mut columns = Vec::new();
4615        loop {
4616            columns.push(self.expect_ident_like()?);
4617            if matches!(self.peek(), Token::Comma) {
4618                self.advance();
4619            } else {
4620                break;
4621            }
4622        }
4623        if !matches!(self.peek(), Token::From) {
4624            return Err(self.err(alloc::format!(
4625                "expected FROM in CREATE STATISTICS, got {:?}",
4626                self.peek()
4627            )));
4628        }
4629        self.advance();
4630        let table = self.expect_ident_like()?;
4631        Ok(Statement::CreateStatistics {
4632            name,
4633            if_not_exists,
4634            kinds,
4635            columns,
4636            table,
4637        })
4638    }
4639
4640    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4641    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4642    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4643    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4644    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4645    /// forward call.
4646    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4647        self.advance(); // TABLE
4648        let if_exists = self.consume_if_exists();
4649        let mut names: Vec<String> = Vec::new();
4650        loop {
4651            names.push(self.expect_ident_like()?);
4652            if matches!(self.peek(), Token::Comma) {
4653                self.advance();
4654                continue;
4655            }
4656            break;
4657        }
4658        if matches!(
4659            self.peek(),
4660            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4661                || s.eq_ignore_ascii_case("restrict")
4662        ) {
4663            self.advance();
4664        }
4665        Ok(Statement::DropTable { names, if_exists })
4666    }
4667
4668    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4669        self.advance(); // STATISTICS
4670        let mut if_exists = false;
4671        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4672            && matches!(self.tokens.get(self.pos + 1),
4673                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4674        {
4675            self.advance();
4676            self.advance();
4677            if_exists = true;
4678        }
4679        let name = self.expect_ident_like()?;
4680        Ok(Statement::DropStatistics { name, if_exists })
4681    }
4682
4683    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4684        debug_assert!(matches!(self.peek(), Token::Create));
4685        self.advance();
4686        match self.peek() {
4687            Token::Table => self.parse_create_table_stmt_after_create(),
4688            Token::Index => self.parse_create_index_stmt_after_create(false),
4689            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4690            // object now. It used to be consumed by the CREATE-noise
4691            // arm, so a pg_dump that declares extended statistics
4692            // restored silently without them and reflection showed
4693            // nothing.
4694            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4695                self.parse_create_statistics_after_create()
4696            }
4697            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4698            // The `UNIQUE` modifier turns a partial index into a
4699            // partial-uniqueness invariant (only rows matching the
4700            // WHERE predicate are checked for duplicates). mailrs
4701            // K1 (3 hits: email_templates default, calendar_events
4702            // master, calendar_events instance).
4703            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4704                self.advance();
4705                if !matches!(self.peek(), Token::Index) {
4706                    return Err(self.err(alloc::format!(
4707                        "expected INDEX after CREATE UNIQUE, got {:?}",
4708                        self.peek()
4709                    )));
4710                }
4711                self.parse_create_index_stmt_after_create(true)
4712            }
4713            Token::Publication => {
4714                self.advance();
4715                self.parse_create_publication_after_keyword()
4716            }
4717            Token::Subscription => {
4718                self.advance();
4719                self.parse_create_subscription_after_keyword()
4720            }
4721            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4722            // USER isn't a reserved keyword — we look for the bare
4723            // identifier so the lexer doesn't have to grow a token.
4724            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4725                self.advance();
4726                self.parse_create_user_after_keyword(true)
4727            }
4728            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4729            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4730            // the default of the LOGIN attribute.
4731            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4732                self.advance();
4733                self.parse_create_user_after_keyword(false)
4734            }
4735            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4736            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4737                self.advance();
4738                self.parse_create_policy_after_keyword()
4739            }
4740            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4741            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4742            // no-op. mailrs follow-up F3.
4743            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4744                self.advance();
4745                self.parse_create_extension_after_keyword()
4746            }
4747            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4748            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4749            // optional; absorb it here and forward to the
4750            // per-kind parsers with the flag. OR is a reserved
4751            // keyword token.
4752            Token::Or => {
4753                self.advance();
4754                let next = self.peek();
4755                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4756                    return Err(self.err(alloc::format!(
4757                        "expected REPLACE after CREATE OR, got {next:?}"
4758                    )));
4759                };
4760                if !s2.eq_ignore_ascii_case("replace") {
4761                    return Err(self.err(alloc::format!(
4762                        "expected REPLACE after CREATE OR, got {s2:?}"
4763                    )));
4764                }
4765                self.advance();
4766                self.parse_create_function_or_trigger_after_or_replace(true)
4767            }
4768            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4769                self.advance();
4770                self.parse_create_function_after_keyword(false)
4771            }
4772            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4773                self.advance();
4774                self.parse_create_trigger_after_keyword(false)
4775            }
4776            // v7.39 (round 139) — CREATE RULE …
4777            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4778                self.advance();
4779                self.parse_create_rule_after_keyword(false)
4780            }
4781            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4782            // trigger is a row-level AFTER trigger that additionally carries
4783            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4784            // path already tolerates and skips those clauses, so consuming the
4785            // CONSTRAINT keyword and reusing it makes the statement parse and the
4786            // trigger fire. (The deferral timing itself is not yet honoured —
4787            // SPG fires it as a plain AFTER trigger, which is correct behaviour
4788            // for every non-deferred use.)
4789            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4790                self.advance();
4791                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4792                    if t.eq_ignore_ascii_case("trigger"))
4793                {
4794                    return Err(self.err(alloc::format!(
4795                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4796                        self.peek()
4797                    )));
4798                }
4799                self.advance();
4800                self.parse_create_trigger_after_keyword(false)
4801            }
4802            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4803            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4804                self.advance();
4805                self.parse_create_sequence_after_keyword(false)
4806            }
4807            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4808            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4809                self.advance();
4810                self.parse_create_view_after_keyword(false, false, false)
4811            }
4812            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4813            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4814            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4815            // appear (in any order) between `CREATE` and `VIEW` in
4816            // every mysqldump-emitted view. Pre-2.6 the parser
4817            // rejected the prefix and the customer's whole view
4818            // backup failed on the first view. The hints are pure
4819            // planner / permission metadata; SPG's view-rewrite
4820            // path is semantically equivalent for all three
4821            // algorithms in v7.17 (TEMPTABLE differs only in
4822            // perf for huge views — out of v7.17 scope), and
4823            // DEFINER / SQL SECURITY are pure single-user
4824            // permissioning that SPG ignores by design.
4825            Token::Ident(s) | Token::QuotedIdent(s)
4826                if s.eq_ignore_ascii_case("algorithm")
4827                    || s.eq_ignore_ascii_case("definer")
4828                    || s.eq_ignore_ascii_case("sql") =>
4829            {
4830                self.consume_mysql_view_prefix()?;
4831                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4832                // (in any order, in any combination), the next
4833                // keyword must be VIEW. mysqldump never emits these
4834                // prefixes on non-view statements.
4835                let next = self.peek().clone();
4836                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4837                    if s2.eq_ignore_ascii_case("view"))
4838                {
4839                    self.advance();
4840                    self.parse_create_view_after_keyword(false, false, false)
4841                } else {
4842                    Err(self.err(alloc::format!(
4843                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4844                    )))
4845                }
4846            }
4847            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4848            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4849                self.advance();
4850                self.parse_create_type_after_keyword()
4851            }
4852            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4853            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4854            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4855                self.advance();
4856                self.parse_create_domain_after_keyword()
4857            }
4858            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4859            // name [AUTHORIZATION user]. Real catalog registry
4860            // (was silent-no-op'd pre-v7.17).
4861            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4862                self.advance();
4863                let if_not_exists = self.parse_if_not_exists();
4864                let name = self.expect_ident_like()?;
4865                // Optional `AUTHORIZATION <user>` trailer — accepted,
4866                // ignored (single-user catalog).
4867                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4868                    if s.eq_ignore_ascii_case("authorization"))
4869                {
4870                    self.advance();
4871                    let _ = self.expect_ident_like()?;
4872                }
4873                Ok(Statement::CreateSchema { name, if_not_exists })
4874            }
4875            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4876            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4877                self.advance();
4878                let next = self.peek().clone();
4879                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4880                {
4881                    self.advance();
4882                    self.parse_create_materialized_view_after_keyword()
4883                } else {
4884                    Err(self.err(alloc::format!(
4885                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4886                    )))
4887                }
4888            }
4889            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4890            // no-op below), an UNLOGGED table is a real, fully-usable table in
4891            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4892            // durability optimisation is a follow-up), so a dump / app that
4893            // declares UNLOGGED tables works instead of failing to parse.
4894            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4895                self.advance(); // UNLOGGED
4896                if matches!(self.peek(), Token::Table) {
4897                    self.parse_create_table_stmt_after_create()
4898                } else {
4899                    Err(self.err(format!(
4900                        "expected TABLE after CREATE UNLOGGED, got {:?}",
4901                        self.peek()
4902                    )))
4903                }
4904            }
4905            Token::Ident(s) | Token::QuotedIdent(s)
4906                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4907            {
4908                self.advance();
4909                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4910                let next = self.peek().clone();
4911                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4912                {
4913                    self.advance();
4914                    self.parse_create_sequence_after_keyword(true)
4915                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4916                {
4917                    self.advance();
4918                    self.parse_create_view_after_keyword(false, false, true)
4919                } else {
4920                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4921                    // consumed and answered OK while creating nothing, so
4922                    // every statement that touched the table afterwards failed
4923                    // with "table not found" — the DDL itself lied. It is a
4924                    // real CREATE TABLE now, marked temporary so the executor
4925                    // puts it in the session's own namespace. An optional
4926                    // TABLE keyword may or may not be present (`CREATE TEMP t`
4927                    // is not legal, but the keyword is consumed by the
4928                    // CREATE TABLE parser itself).
4929                    let stmt = self.parse_create_table_stmt_after_create()?;
4930                    match stmt {
4931                        Statement::CreateTable(mut c) => {
4932                            c.temporary = true;
4933                            Ok(Statement::CreateTable(c))
4934                        }
4935                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4936                        // CTAS node, which needs the same session namespace.
4937                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4938                            m.temporary = true;
4939                            Ok(Statement::CreateMaterializedView(m))
4940                        }
4941                        other => Ok(other),
4942                    }
4943                }
4944            }
4945            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4946            // BEGIN <body> END`. The body may reference `@var`
4947            // session variables, SET statements, internal `;`
4948            // terminators, etc. SPG has no procedure runtime, so
4949            // consume the whole `CREATE PROCEDURE … END` block as
4950            // a no-op so mysqldump scripts that include stored
4951            // routines load through. The matching-END consumer
4952            // tracks BEGIN/END nesting depth to handle nested
4953            // BEGIN blocks correctly.
4954            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4955                self.consume_mysql_routine_body();
4956                Ok(Statement::Empty)
4957            }
4958            // v7.14.0 — pg_dump / mysqldump emit
4959            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4960            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4961            // SPG is single-schema / single-database; these have
4962            // no behavioural effect, so consume + return Empty.
4963            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4964            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4965            // moved up to real parser branches. DATABASE / ROLE /
4966            // POLICY / OPERATOR stay no-op forever
4967            // (single-database, hardcoded roles).
4968            Token::Ident(s) | Token::QuotedIdent(s)
4969                if matches!(
4970                    s.to_ascii_lowercase().as_str(),
4971                    "database"
4972                        | "role"
4973                        | "operator"
4974                        | "cast"
4975                        | "aggregate"
4976                        | "language"
4977                        | "collation"
4978                        | "conversion"
4979                        // v7.17.0 Phase 8 (audit N6) — rarely-
4980                        // emitted pg_dump shapes that should
4981                        // load through without a parser error.
4982                        // SPG has no planner statistics catalog,
4983                        // no event-trigger hooks, no foreign-
4984                        // data-wrapper infrastructure; consume
4985                        // + return Empty.
4986                        | "statistics"
4987                        | "event"
4988                        // v7.37.17 (17.6 siblings) — additional CREATE
4989                        // targets pg_dump / operator install scripts
4990                        // may emit that SPG has no matching machinery
4991                        // for. Consume + Empty-return.
4992                        | "text"
4993                        | "tablespace"
4994                        | "access"
4995                        | "large"
4996                ) =>
4997            {
4998                // DATABASE is the one member of this list PG refuses
4999                // inside a transaction block; the rest (ROLE, CAST,
5000                // TABLESPACE, …) it runs there quite happily, so only
5001                // this one is named. Still a no-op otherwise — SPG is
5002                // single-database.
5003                let is_database = s.eq_ignore_ascii_case("database");
5004                // The name is the first token after DATABASE, past an
5005                // `IF NOT EXISTS`.
5006                let name = if is_database {
5007                    self.scan_database_name()
5008                } else {
5009                    None
5010                };
5011                let collation = if is_database {
5012                    self.scan_database_collation_until_boundary()
5013                } else {
5014                    self.consume_until_statement_boundary();
5015                    None
5016                };
5017                if is_database {
5018                    return Ok(Statement::NoOpPreventedInTransaction {
5019                        what: String::from("CREATE DATABASE"),
5020                        collation,
5021                        name,
5022                    });
5023                }
5024                Ok(Statement::Empty)
5025            }
5026            // v7.39 (round 706) — the foreign-data family leaves the silent
5027            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5028            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5029            // FDW machinery), but the ENGINE now warns, so a restore log
5030            // says what will not function instead of reporting success.
5031            Token::Ident(s) | Token::QuotedIdent(s)
5032                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5033            {
5034                self.consume_until_statement_boundary();
5035                Ok(Statement::ValidateOnly {
5036                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5037                    names: Vec::new(),
5038                })
5039            }
5040            other => Err(self.err(format!(
5041                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5042            ))),
5043        }
5044    }
5045
5046    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5047    /// keyword decides whether we parse a function or trigger
5048    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5049    /// PROCEDURE) — those land in later releases.
5050    fn parse_create_function_or_trigger_after_or_replace(
5051        &mut self,
5052        or_replace: bool,
5053    ) -> Result<Statement, ParseError> {
5054        let tok = self.peek();
5055        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5056            return Err(self.err(alloc::format!(
5057                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5058            )));
5059        };
5060        if s.eq_ignore_ascii_case("function") {
5061            self.advance();
5062            self.parse_create_function_after_keyword(or_replace)
5063        } else if s.eq_ignore_ascii_case("trigger") {
5064            self.advance();
5065            self.parse_create_trigger_after_keyword(or_replace)
5066        } else if s.eq_ignore_ascii_case("rule") {
5067            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5068            self.advance();
5069            self.parse_create_rule_after_keyword(or_replace)
5070        } else if s.eq_ignore_ascii_case("view") {
5071            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5072            self.advance();
5073            self.parse_create_view_after_keyword(or_replace, false, false)
5074        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5075            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5076            self.advance();
5077            let nxt = self.peek().clone();
5078            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5079            {
5080                self.advance();
5081                self.parse_create_view_after_keyword(or_replace, false, true)
5082            } else {
5083                Err(self.err(alloc::format!(
5084                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5085                )))
5086            }
5087        } else {
5088            Err(self.err(alloc::format!(
5089                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5090            )))
5091        }
5092    }
5093
5094    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5095    /// SPG doesn't have a registry; pgvector / similar are
5096    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5097    /// the syntax lets dual-target schemas keep the line.
5098    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5099        // Optional `IF NOT EXISTS`.
5100        self.consume_if_not_exists();
5101        let name = self.expect_ident_like()?;
5102        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5103        // CASCADE / FROM '<v>' clauses; we don't model them.
5104        loop {
5105            match self.peek() {
5106                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5107                    self.advance();
5108                    continue;
5109                }
5110                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5111                    self.advance();
5112                    let _ = self.expect_ident_like()?;
5113                    continue;
5114                }
5115                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5116                    self.advance();
5117                    // String or ident literal.
5118                    let _ = self.advance();
5119                    continue;
5120                }
5121                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5122                    self.advance();
5123                    let _ = self.advance();
5124                    continue;
5125                }
5126                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5127                    self.advance();
5128                    continue;
5129                }
5130                _ => break,
5131            }
5132        }
5133        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5134        // nosuch` reported success and `pg_extension` then did not list it,
5135        // which is the accept-and-do-nothing shape F31 exists to find.
5136        Ok(Statement::ValidateOnly {
5137            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5138            names: alloc::vec![name],
5139        })
5140    }
5141
5142    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5143    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5144    /// already been consumed by the caller. Grammar accepted:
5145    ///
5146    ///   name `(` arg-list `)`
5147    ///   `RETURNS` return-type
5148    ///   [ `LANGUAGE` ident ]
5149    ///   `AS` $$ body $$
5150    ///   [ `LANGUAGE` ident ]
5151    ///
5152    /// Either `LANGUAGE` position is allowed; PG accepts both.
5153    fn parse_create_function_after_keyword(
5154        &mut self,
5155        or_replace: bool,
5156    ) -> Result<Statement, ParseError> {
5157        let name = self.expect_ident_like()?;
5158        // Argument list. v7.12.4 commonly sees the empty `()`
5159        // (trigger functions); typed args parse and round-trip
5160        // but the executor only invokes nullary functions.
5161        if !matches!(self.peek(), Token::LParen) {
5162            return Err(self.err(alloc::format!(
5163                "expected '(' after function name {name:?}, got {:?}",
5164                self.peek()
5165            )));
5166        }
5167        self.advance();
5168        let args = self.parse_function_arg_list()?;
5169        // RETURNS clause.
5170        let tok = self.peek();
5171        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5172            return Err(self.err(alloc::format!(
5173                "expected RETURNS after function arg list, got {tok:?}"
5174            )));
5175        };
5176        if !s.eq_ignore_ascii_case("returns") {
5177            return Err(self.err(alloc::format!(
5178                "expected RETURNS after function arg list, got {s:?}"
5179            )));
5180        }
5181        self.advance();
5182        let returns = self.parse_function_return()?;
5183        // Optional LANGUAGE clause (PG also accepts after AS — we'll
5184        // re-check after the body too).
5185        let mut language: Option<String> = self.parse_optional_language()?;
5186        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5187        // either side of the body and in any order, interleaved with
5188        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5189        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5190        // PG's own pg_dump output did not restore.
5191        let mut attrs = FunctionAttrs::default();
5192        loop {
5193            let before = self.pos;
5194            self.parse_function_attrs_into(&mut attrs)?;
5195            if language.is_none() {
5196                language = self.parse_optional_language()?;
5197            }
5198            if self.pos == before {
5199                break;
5200            }
5201        }
5202        // `AS` followed by a $$-quoted body (lexer already
5203        // collapses both `$$…$$` and `$tag$…$tag$` to a single
5204        // Token::String). AS is a reserved keyword (Token::As).
5205        if !matches!(self.peek(), Token::As) {
5206            return Err(self.err(alloc::format!(
5207                "expected AS before function body, got {:?}",
5208                self.peek()
5209            )));
5210        }
5211        self.advance();
5212        let body_text = match self.peek() {
5213            Token::String(s) => {
5214                let body = s.clone();
5215                self.advance();
5216                body
5217            }
5218            other => {
5219                return Err(self.err(alloc::format!(
5220                    "expected $$-quoted function body after AS, got {other:?}"
5221                )));
5222            }
5223        };
5224        // Trailing clauses — PG's other accepted position for both the
5225        // LANGUAGE and the attributes.
5226        loop {
5227            let before = self.pos;
5228            self.parse_function_attrs_into(&mut attrs)?;
5229            if language.is_none() {
5230                language = self.parse_optional_language()?;
5231            }
5232            if self.pos == before {
5233                break;
5234            }
5235        }
5236        let language = language.unwrap_or_else(|| String::from("sql"));
5237        // PL/pgSQL bodies get structure-parsed. Other languages
5238        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5239        // recognise) round-trip as Raw text — the executor errors
5240        // when invoked with a clear unsupported message.
5241        let body = if language.eq_ignore_ascii_case("plpgsql") {
5242            match parse_plpgsql_body(&body_text) {
5243                Ok(block) => FunctionBody::PlPgSql(block),
5244                // Best-effort: if the body parser doesn't yet
5245                // support a construct used inside, fall back to
5246                // raw — keeps `CREATE FUNCTION` itself working
5247                // (catalogue accepts), executor errors on
5248                // invocation only.
5249                Err(_) => FunctionBody::Raw(body_text),
5250            }
5251        } else {
5252            FunctionBody::Raw(body_text)
5253        };
5254        Ok(Statement::CreateFunction(CreateFunctionStatement {
5255            name,
5256            or_replace,
5257            args,
5258            returns,
5259            language,
5260            body,
5261            attrs,
5262        }))
5263    }
5264
5265    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5266    /// attribute clauses into `attrs`, stopping at the first token that
5267    /// is not one. Measured against PG 18.4, which accepts them in any
5268    /// order and on either side of the body.
5269    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5270        loop {
5271            let word = match self.peek() {
5272                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5273                // NOT LEAKPROOF — NOT is a reserved keyword token.
5274                Token::Not
5275                    if matches!(
5276                        self.tokens.get(self.pos + 1),
5277                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5278                    ) =>
5279                {
5280                    self.advance();
5281                    self.advance();
5282                    attrs.leakproof = false;
5283                    continue;
5284                }
5285                _ => return Ok(()),
5286            };
5287            match word.as_str() {
5288                "immutable" => {
5289                    self.advance();
5290                    attrs.volatility = FunctionVolatility::Immutable;
5291                }
5292                "stable" => {
5293                    self.advance();
5294                    attrs.volatility = FunctionVolatility::Stable;
5295                }
5296                "volatile" => {
5297                    self.advance();
5298                    attrs.volatility = FunctionVolatility::Volatile;
5299                }
5300                "strict" => {
5301                    self.advance();
5302                    attrs.strict = true;
5303                }
5304                "leakproof" => {
5305                    self.advance();
5306                    attrs.leakproof = true;
5307                }
5308                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5309                // spelled-out forms of STRICT and its opposite.
5310                "returns" | "called" => {
5311                    let strict = word == "returns";
5312                    let mut probe = self.pos + 1;
5313                    if strict {
5314                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5315                        // is not ours.
5316                        match self.tokens.get(probe) {
5317                            Some(Token::Null) => probe += 1,
5318                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5319                            _ => return Ok(()),
5320                        }
5321                    }
5322                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5323                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5324                    if !ok {
5325                        return Ok(());
5326                    }
5327                    probe += 1;
5328                    match self.tokens.get(probe) {
5329                        Some(Token::Null) => probe += 1,
5330                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5331                        _ => return Ok(()),
5332                    }
5333                    match self.tokens.get(probe) {
5334                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5335                        _ => return Ok(()),
5336                    }
5337                    self.pos = probe;
5338                    attrs.strict = strict;
5339                }
5340                "security" | "external" => {
5341                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5342                    let mut probe = self.pos + 1;
5343                    if word == "external" {
5344                        match self.tokens.get(probe) {
5345                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5346                                probe += 1;
5347                            }
5348                            _ => return Ok(()),
5349                        }
5350                    }
5351                    let definer = match self.tokens.get(probe) {
5352                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5353                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5354                        _ => return Ok(()),
5355                    };
5356                    self.pos = probe + 1;
5357                    attrs.security_definer = definer;
5358                }
5359                "parallel" => {
5360                    let level = match self.tokens.get(self.pos + 1) {
5361                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5362                            FunctionParallel::Safe
5363                        }
5364                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5365                            FunctionParallel::Restricted
5366                        }
5367                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5368                            FunctionParallel::Unsafe
5369                        }
5370                        _ => return Ok(()),
5371                    };
5372                    self.pos += 2;
5373                    attrs.parallel = level;
5374                }
5375                "cost" | "rows" => {
5376                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5377                        return Ok(());
5378                    };
5379                    self.pos += 2;
5380                    if word == "cost" {
5381                        attrs.cost = Some(n);
5382                    } else {
5383                        attrs.rows = Some(n);
5384                    }
5385                }
5386                _ => return Ok(()),
5387            }
5388        }
5389    }
5390
5391    /// The numeric literal at `idx`, if there is one.
5392    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5393        match self.tokens.get(idx)? {
5394            Token::Integer(n) => Some(*n as f64),
5395            Token::Float(f) => Some(*f),
5396            Token::Numeric(t) => t.parse::<f64>().ok(),
5397            _ => None,
5398        }
5399    }
5400
5401    /// Closing `)`-terminated argument list. v7.12.4 commonly
5402    /// sees the empty `()`; typed args round-trip but the
5403    /// executor (yet) doesn't invoke them.
5404    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5405    /// it away, which is what PG does with one on a function parameter.
5406    fn skip_type_modifier(&mut self) {
5407        if !matches!(self.peek(), Token::LParen) {
5408            return;
5409        }
5410        // Only a numeric modifier — anything else is not one, and eating
5411        // it would swallow real grammar.
5412        let mut i = self.pos + 1;
5413        let mut seen_number = false;
5414        loop {
5415            match self.tokens.get(i) {
5416                Some(Token::Integer(_)) => seen_number = true,
5417                Some(Token::Comma) => {}
5418                Some(Token::RParen) => break,
5419                _ => return,
5420            }
5421            i += 1;
5422        }
5423        if !seen_number {
5424            return;
5425        }
5426        while self.pos <= i {
5427            self.advance();
5428        }
5429    }
5430
5431    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5432        let mut args: Vec<FunctionArg> = Vec::new();
5433        if matches!(self.peek(), Token::RParen) {
5434            self.advance();
5435            return Ok(args);
5436        }
5437        loop {
5438            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5439            // a reserved token; OUT / INOUT are bare idents.
5440            let mode = if matches!(self.peek(), Token::In) {
5441                self.advance();
5442                FunctionArgMode::In
5443            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5444            {
5445                self.advance();
5446                FunctionArgMode::Out
5447            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5448            {
5449                self.advance();
5450                FunctionArgMode::InOut
5451            } else {
5452                FunctionArgMode::In
5453            };
5454            // Optional name. The next token is either a name
5455            // (followed by a type ident) or the type itself.
5456            // Disambiguate by peeking ahead: if the token after
5457            // the next ident is also an ident, we treat the
5458            // first as the name.
5459            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5460            // the comma or paren, then decide. Reading at most two of
5461            // them could not spell `x double precision` at all, and
5462            // silently mis-read the bare `double precision` as a
5463            // parameter named "double" — which is what made the same
5464            // signature key two different ways.
5465            let (name, ty_token) = {
5466                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5467                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5468                    words.push(self.expect_ident_like()?);
5469                }
5470                // v7.39 (round 344) — a length / precision modifier on the
5471                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5472                // accepts it and DROPS it — `pg_get_function_arguments`
5473                // reports plain `character varying` / `numeric`, measured on
5474                // 18.4 — but SPG raised `syntax error at or near "("`,
5475                // because the modifier's parens were never consumed.
5476                self.skip_type_modifier();
5477                // r1049 — `f(v bigint[])`. The array suffix parsed in
5478                // the column position, the cast position and (r1038)
5479                // the RETURNS position, but not here: the fifth
5480                // member of the same family, reported by sentori as
5481                // presumably the same code. It is now.
5482                let array_suffix = self.consume_array_suffix();
5483                let whole = words.join(" ");
5484                let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5485                {
5486                    (Some(words[0].clone()), words[1..].join(" "))
5487                } else {
5488                    (None, whole)
5489                };
5490                ty_token.push_str(&array_suffix);
5491                (name, ty_token)
5492            };
5493            // Type — try to map to ColumnTypeName, else Raw.
5494            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5495                Some(t) => FunctionArgType::Typed(t),
5496                None => FunctionArgType::Raw(ty_token),
5497            };
5498            args.push(FunctionArg { mode, name, ty });
5499            match self.peek() {
5500                Token::Comma => {
5501                    self.advance();
5502                    continue;
5503                }
5504                Token::RParen => {
5505                    self.advance();
5506                    return Ok(args);
5507                }
5508                other => {
5509                    return Err(self.err(alloc::format!(
5510                        "expected , or ) in function arg list, got {other:?}"
5511                    )));
5512                }
5513            }
5514        }
5515    }
5516
5517    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5518        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5519        // function whose row shape is named inline.
5520        if matches!(self.peek(), Token::Table)
5521            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5522        {
5523            self.advance(); // TABLE
5524            self.advance(); // (
5525            let mut cols: Vec<String> = Vec::new();
5526            loop {
5527                let cname = self.expect_ident_like()?;
5528                let mut ty: Vec<String> = Vec::new();
5529                loop {
5530                    match self.peek() {
5531                        Token::Comma | Token::RParen | Token::Eof => break,
5532                        _ => {}
5533                    }
5534                    match self.advance() {
5535                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5536                        other => {
5537                            if let Some(w) = unreserved_keyword_text(&other) {
5538                                ty.push(w);
5539                            }
5540                        }
5541                    }
5542                }
5543                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5544                if matches!(self.peek(), Token::Comma) {
5545                    self.advance();
5546                } else {
5547                    break;
5548                }
5549            }
5550            if matches!(self.peek(), Token::RParen) {
5551                self.advance();
5552            }
5553            return Ok(FunctionReturn::Other(alloc::format!(
5554                "TABLE({})",
5555                cols.join(", ")
5556            )));
5557        }
5558        let ident = self.expect_ident_like()?;
5559        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5560        if ident.eq_ignore_ascii_case("setof") {
5561            let inner = self.expect_ident_like()?;
5562            let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5563            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5564        }
5565        if ident.eq_ignore_ascii_case("trigger") {
5566            return Ok(FunctionReturn::Trigger);
5567        }
5568        if ident.eq_ignore_ascii_case("void") {
5569            return Ok(FunctionReturn::Void);
5570        }
5571        // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5572        // RETURN position did not, so the `[` was a syntax error and the
5573        // whole migration stopped. sentori worked around it by returning
5574        // zero-padded text.
5575        let suffix = self.consume_array_suffix();
5576        if !suffix.is_empty() {
5577            return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5578        }
5579        match map_type_ident_to_column_type_name(&ident) {
5580            Some(t) => Ok(FunctionReturn::Type(t)),
5581            None => Ok(FunctionReturn::Other(ident)),
5582        }
5583    }
5584
5585    /// Consume any `[]` / `[N]` array markers after a type name and give
5586    /// back their text. Empty when there are none.
5587    fn consume_array_suffix(&mut self) -> String {
5588        let mut out = String::new();
5589        while matches!(self.peek(), Token::LBracket) {
5590            self.advance();
5591            // `[N]` is accepted and, as in PG, the length is not enforced.
5592            if let Token::Integer(n) = self.peek().clone() {
5593                self.advance();
5594                out.push_str(&alloc::format!("[{n}]"));
5595            } else {
5596                out.push_str("[]");
5597            }
5598            if matches!(self.peek(), Token::RBracket) {
5599                self.advance();
5600            }
5601        }
5602        out
5603    }
5604
5605    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5606        match self.peek() {
5607            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5608                self.advance();
5609                let lang = self.expect_ident_like()?;
5610                Ok(Some(lang.to_ascii_lowercase()))
5611            }
5612            _ => Ok(None),
5613        }
5614    }
5615
5616    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5617    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5618    /// (expr)]*`. The `DOMAIN` keyword has already been
5619    /// consumed. PG allows the trailing constraints in any
5620    /// order; we approximate with a small loop.
5621    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5622        let name = self.expect_ident_like()?;
5623        // Optional `AS`.
5624        if matches!(self.peek(), Token::As) {
5625            self.advance();
5626        }
5627        // v7.39 (round 259) — keep the raw type NAME when the base is not
5628        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5629        // parent domain.
5630        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5631            self.parse_type_with_implied_flags()?;
5632        let mut default: Option<Expr> = None;
5633        let mut not_null = false;
5634        let mut checks: Vec<Expr> = Vec::new();
5635        loop {
5636            match self.peek() {
5637                Token::Default => {
5638                    if default.is_some() {
5639                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5640                    }
5641                    self.advance();
5642                    default = Some(self.parse_expr(0)?);
5643                }
5644                Token::Not => {
5645                    self.advance();
5646                    if !matches!(self.peek(), Token::Null) {
5647                        return Err(self.err(alloc::format!(
5648                            "expected NULL after NOT in DOMAIN, got {:?}",
5649                            self.peek()
5650                        )));
5651                    }
5652                    self.advance();
5653                    not_null = true;
5654                }
5655                Token::Null => {
5656                    self.advance();
5657                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5658                    // is the default-nullable marker (PG accepts it),
5659                    // but AFTER a NOT NULL it is a conflict PG refuses
5660                    // (`conflicting NULL/NOT NULL constraints`,
5661                    // PG18-measured); the old arm no-opped both ways.
5662                    if not_null {
5663                        return Err(self.err(alloc::string::String::from(
5664                            "conflicting NULL/NOT NULL constraints",
5665                        )));
5666                    }
5667                }
5668                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5669                    self.advance();
5670                    if !matches!(self.peek(), Token::LParen) {
5671                        return Err(self.err(alloc::format!(
5672                            "expected '(' after CHECK in DOMAIN, got {:?}",
5673                            self.peek()
5674                        )));
5675                    }
5676                    self.advance();
5677                    let expr = self.parse_expr(0)?;
5678                    if !matches!(self.peek(), Token::RParen) {
5679                        return Err(self.err(alloc::format!(
5680                            "expected ')' after CHECK expr, got {:?}",
5681                            self.peek()
5682                        )));
5683                    }
5684                    self.advance();
5685                    checks.push(expr);
5686                }
5687                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5688                // prefix on the constraint; we drop the name and
5689                // recurse into the constraint parsing.
5690                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5691                    self.advance();
5692                    let _ = self.expect_ident_like()?;
5693                }
5694                _ => break,
5695            }
5696        }
5697        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5698            name,
5699            base_type,
5700            base_domain: base_user_ref,
5701            default,
5702            not_null,
5703            checks,
5704        }))
5705    }
5706
5707    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5708    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5709    /// consumed.
5710    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5711        let name = self.expect_ident_like()?;
5712        // Required `AS`.
5713        if !matches!(self.peek(), Token::As) {
5714            return Err(self.err(alloc::format!(
5715                "expected AS after CREATE TYPE {name:?}, got {:?}",
5716                self.peek()
5717            )));
5718        }
5719        self.advance();
5720        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5721        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5722        // on the next token: `(` = composite, ident `ENUM` = enum.
5723        if matches!(self.peek(), Token::LParen) {
5724            self.advance();
5725            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5726            let mut field_user_types: Vec<Option<String>> = Vec::new();
5727            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5728            // is legal PG (an attribute-less composite; measured — the old
5729            // e2e note claimed PG requires at least one attribute).
5730            if matches!(self.peek(), Token::RParen) {
5731                self.advance();
5732                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5733                    name,
5734                    kind: crate::ast::TypeKind::Composite {
5735                        fields,
5736                        field_user_types,
5737                    },
5738                }));
5739            }
5740            loop {
5741                let field_name = self.expect_ident_like()?;
5742                // v7.39 (round 264) — keep the raw type name when it is not
5743                // a builtin: that is how a NESTED composite field records
5744                // which composite it holds.
5745                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5746                    self.parse_type_with_implied_flags()?;
5747                fields.push((field_name, field_type));
5748                field_user_types.push(field_user_ref);
5749                if matches!(self.peek(), Token::Comma) {
5750                    self.advance();
5751                    continue;
5752                }
5753                if matches!(self.peek(), Token::RParen) {
5754                    self.advance();
5755                    break;
5756                }
5757                return Err(self.err(alloc::format!(
5758                    "expected , or ) in composite field list, got {:?}",
5759                    self.peek()
5760                )));
5761            }
5762            if fields.is_empty() {
5763                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5764            }
5765            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5766                name,
5767                kind: crate::ast::TypeKind::Composite {
5768                    fields,
5769                    field_user_types,
5770                },
5771            }));
5772        }
5773        // Required `ENUM` ident.
5774        let kind_ident = match self.peek().clone() {
5775            Token::Ident(s) | Token::QuotedIdent(s) => s,
5776            other => {
5777                return Err(self.err(alloc::format!(
5778                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5779                )));
5780            }
5781        };
5782        if !kind_ident.eq_ignore_ascii_case("enum") {
5783            return Err(self.err(alloc::format!(
5784                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5785            )));
5786        }
5787        self.advance();
5788        if !matches!(self.peek(), Token::LParen) {
5789            return Err(self.err(alloc::format!(
5790                "expected '(' after ENUM, got {:?}",
5791                self.peek()
5792            )));
5793        }
5794        self.advance();
5795        let mut labels: Vec<String> = Vec::new();
5796        loop {
5797            match self.peek().clone() {
5798                Token::String(s) => {
5799                    self.advance();
5800                    labels.push(s);
5801                }
5802                other => {
5803                    return Err(
5804                        self.err(alloc::format!("expected enum label string, got {other:?}"))
5805                    );
5806                }
5807            }
5808            if matches!(self.peek(), Token::Comma) {
5809                self.advance();
5810                continue;
5811            }
5812            if matches!(self.peek(), Token::RParen) {
5813                self.advance();
5814                break;
5815            }
5816            return Err(self.err(alloc::format!(
5817                "expected , or ) in ENUM label list, got {:?}",
5818                self.peek()
5819            )));
5820        }
5821        if labels.is_empty() {
5822            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5823        }
5824        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5825            name,
5826            kind: crate::ast::TypeKind::Enum { labels },
5827        }))
5828    }
5829
5830    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5831    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5832    /// The `CREATE MATERIALIZED VIEW` keywords have already been
5833    /// consumed.
5834    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5835        let if_not_exists = self.parse_if_not_exists();
5836        let name = self.expect_ident_like()?;
5837        let mut columns: Vec<String> = Vec::new();
5838        if matches!(self.peek(), Token::LParen) {
5839            self.advance();
5840            loop {
5841                let c = self.expect_ident_like()?;
5842                columns.push(c);
5843                if matches!(self.peek(), Token::Comma) {
5844                    self.advance();
5845                    continue;
5846                }
5847                if matches!(self.peek(), Token::RParen) {
5848                    self.advance();
5849                    break;
5850                }
5851                return Err(self.err(alloc::format!(
5852                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5853                    self.peek()
5854                )));
5855            }
5856        }
5857        if !matches!(self.peek(), Token::As) {
5858            return Err(self.err(alloc::format!(
5859                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5860                self.peek()
5861            )));
5862        }
5863        self.advance();
5864        // v7.39 (round 151) — a WITH-headed body is legal (read-only
5865        // CTEs only; the engine rejects data-modifying ones with PG's
5866        // message). A trailing `WITH [NO] DATA` can't START the body,
5867        // so WITH here heads the query.
5868        let body = if self.peek_is_with_kw() {
5869            self.advance();
5870            self.parse_nested_with_select()?
5871        } else {
5872            let body_stmt = self.parse_select_stmt()?;
5873            let Statement::Select(body) = body_stmt else {
5874                return Err(self.err(alloc::format!(
5875                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5876                )));
5877            };
5878            body
5879        };
5880        // Optional trailing `WITH [NO] DATA`.
5881        let with_data = self.parse_optional_with_data(true)?;
5882        Ok(Statement::CreateMaterializedView(
5883            crate::ast::CreateMaterializedViewStatement {
5884                temporary: false,
5885                name,
5886                if_not_exists,
5887                columns,
5888                body,
5889                with_data,
5890                as_plain_table: false,
5891            },
5892        ))
5893    }
5894
5895    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5896    /// `default_when_absent` is what to return if the tail is
5897    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5898    /// WITH DATA).
5899    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5900        let save = self.pos;
5901        // `WITH` is an Ident (not reserved in the lexer).
5902        let is_with = match self.peek() {
5903            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5904            _ => false,
5905        };
5906        if !is_with {
5907            return Ok(default_when_absent);
5908        }
5909        self.advance();
5910        // Optional `NO`.
5911        let mut with_data = true;
5912        let is_no = match self.peek() {
5913            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5914            _ => false,
5915        };
5916        if is_no {
5917            self.advance();
5918            with_data = false;
5919        }
5920        // Required `DATA` ident.
5921        let is_data = match self.peek() {
5922            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5923            _ => false,
5924        };
5925        if is_data {
5926            self.advance();
5927            Ok(with_data)
5928        } else {
5929            // Caller's WITH wasn't WITH-DATA — rewind so the outer
5930            // parser can interpret it.
5931            self.pos = save;
5932            Ok(default_when_absent)
5933        }
5934    }
5935
5936    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5937    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5938    /// All keyword prefixes have already been consumed; the flags
5939    /// say which were present.
5940    fn parse_create_view_after_keyword(
5941        &mut self,
5942        or_replace: bool,
5943        _materialized_unused: bool,
5944        temporary: bool,
5945    ) -> Result<Statement, ParseError> {
5946        let if_not_exists = self.parse_if_not_exists();
5947        let name = self.expect_ident_like()?;
5948        // Optional `(col, col, …)` rename list.
5949        let mut columns: Vec<String> = Vec::new();
5950        if matches!(self.peek(), Token::LParen) {
5951            self.advance();
5952            loop {
5953                let c = self.expect_ident_like()?;
5954                columns.push(c);
5955                if matches!(self.peek(), Token::Comma) {
5956                    self.advance();
5957                    continue;
5958                }
5959                if matches!(self.peek(), Token::RParen) {
5960                    self.advance();
5961                    break;
5962                }
5963                return Err(self.err(alloc::format!(
5964                    "expected , or ) in VIEW column list, got {:?}",
5965                    self.peek()
5966                )));
5967            }
5968        }
5969        // Required `AS`.
5970        if !matches!(self.peek(), Token::As) {
5971            return Err(self.err(alloc::format!(
5972                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5973                self.peek()
5974            )));
5975        }
5976        self.advance();
5977        // Body: a regular SELECT statement. v7.39 (round 151) — a
5978        // WITH-headed body is legal too (read-only CTEs only; the
5979        // engine rejects data-modifying ones with PG's message).
5980        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5981        // with the check-option clause, so WITH here heads the query.
5982        let body = if self.peek_is_with_kw() {
5983            self.advance();
5984            self.parse_nested_with_select()?
5985        } else {
5986            let body_stmt = self.parse_select_stmt()?;
5987            let Statement::Select(body) = body_stmt else {
5988                return Err(self.err(alloc::format!(
5989                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5990                )));
5991            };
5992            body
5993        };
5994        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5995        // The SELECT parser stops before a trailing WITH, so it lands here.
5996        let check_option = if matches!(self.peek(),
5997            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5998        {
5999            self.advance(); // WITH
6000            let opt = match self.peek() {
6001                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6002                    self.advance();
6003                    crate::ast::ViewCheckOption::Local
6004                }
6005                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6006                    self.advance();
6007                    crate::ast::ViewCheckOption::Cascaded
6008                }
6009                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6010                _ => crate::ast::ViewCheckOption::Cascaded,
6011            };
6012            if !matches!(self.peek(),
6013                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6014            {
6015                return Err(self.err(alloc::format!(
6016                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6017                    self.peek()
6018                )));
6019            }
6020            self.advance(); // CHECK
6021            if !matches!(self.peek(),
6022                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6023            {
6024                return Err(self.err(alloc::format!(
6025                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6026                    self.peek()
6027                )));
6028            }
6029            self.advance(); // OPTION
6030            Some(opt)
6031        } else {
6032            None
6033        };
6034        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6035            name,
6036            or_replace,
6037            if_not_exists,
6038            temporary,
6039            columns,
6040            body,
6041            check_option,
6042        }))
6043    }
6044
6045    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6046    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6047    /// consumed; `temporary` carries whether TEMPORARY was seen.
6048    fn parse_create_sequence_after_keyword(
6049        &mut self,
6050        temporary: bool,
6051    ) -> Result<Statement, ParseError> {
6052        let if_not_exists = self.parse_if_not_exists();
6053        let name = self.expect_ident_like()?;
6054        // Optional `AS data_type`.
6055        let data_type = if matches!(self.peek(), Token::As) {
6056            self.advance();
6057            Some(self.parse_sequence_data_type()?)
6058        } else {
6059            None
6060        };
6061        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6062        Ok(Statement::CreateSequence(
6063            crate::ast::CreateSequenceStatement {
6064                name,
6065                if_not_exists,
6066                temporary,
6067                data_type,
6068                options,
6069            },
6070        ))
6071    }
6072
6073    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6074    /// already been consumed; this is reached after `SEQUENCE`.
6075    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6076    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6077        use crate::ast::AlterDomainAction as A;
6078        let name = self.expect_ident_like()?;
6079        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6080        let kw = match self.peek() {
6081            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6082            Token::Drop => alloc::string::String::from("drop"),
6083            Token::Default => alloc::string::String::from("default"),
6084            other => {
6085                return Err(self.err(alloc::format!(
6086                    "expected an ALTER DOMAIN action, got {other:?}"
6087                )));
6088            }
6089        };
6090        let action = match kw.as_str() {
6091            "add" => {
6092                self.advance();
6093                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6094                {
6095                    self.advance();
6096                    Some(self.expect_ident_like()?)
6097                } else {
6098                    None
6099                };
6100                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6101                    return Err(self.err(alloc::format!(
6102                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6103                        self.peek()
6104                    )));
6105                }
6106                self.advance();
6107                if !matches!(self.peek(), Token::LParen) {
6108                    return Err(self.err("expected '(' after CHECK".into()));
6109                }
6110                self.advance();
6111                let check = self.parse_expr(0)?;
6112                if !matches!(self.peek(), Token::RParen) {
6113                    return Err(self.err("expected ')' after CHECK expression".into()));
6114                }
6115                self.advance();
6116                A::AddConstraint { name: cname, check }
6117            }
6118            "drop" => {
6119                self.advance();
6120                match self.peek() {
6121                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6122                        self.advance();
6123                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6124                        {
6125                            self.advance();
6126                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6127                            {
6128                                return Err(self.err("expected EXISTS after IF".into()));
6129                            }
6130                            self.advance();
6131                            true
6132                        } else {
6133                            false
6134                        };
6135                        let cn = self.expect_ident_like()?;
6136                        A::DropConstraint {
6137                            name: cn,
6138                            if_exists,
6139                        }
6140                    }
6141                    Token::Default => {
6142                        self.advance();
6143                        A::DropDefault
6144                    }
6145                    Token::Not => {
6146                        self.advance();
6147                        if !matches!(self.peek(), Token::Null) {
6148                            return Err(self.err("expected NULL after NOT".into()));
6149                        }
6150                        self.advance();
6151                        A::DropNotNull
6152                    }
6153                    other => {
6154                        return Err(self.err(alloc::format!(
6155                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6156                        )));
6157                    }
6158                }
6159            }
6160            "set" => {
6161                self.advance();
6162                match self.peek() {
6163                    Token::Default => {
6164                        self.advance();
6165                        A::SetDefault(self.parse_expr(0)?)
6166                    }
6167                    Token::Not => {
6168                        self.advance();
6169                        if !matches!(self.peek(), Token::Null) {
6170                            return Err(self.err("expected NULL after NOT".into()));
6171                        }
6172                        self.advance();
6173                        A::SetNotNull
6174                    }
6175                    other => {
6176                        return Err(self.err(alloc::format!(
6177                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6178                        )));
6179                    }
6180                }
6181            }
6182            "rename" => {
6183                self.advance();
6184                if !matches!(self.peek(), Token::To) {
6185                    return Err(self.err("expected TO after RENAME".into()));
6186                }
6187                self.advance();
6188                A::RenameTo(self.expect_ident_like()?)
6189            }
6190            other => {
6191                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6192            }
6193        };
6194        Ok(Statement::AlterDomain { name, action })
6195    }
6196
6197    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6198        let if_exists = self.parse_if_exists();
6199        let name = self.expect_ident_like()?;
6200        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6201        // the option list (PG allows only one or the other).
6202        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6203            self.advance();
6204            if matches!(self.peek(), Token::To) {
6205                self.advance();
6206            } else {
6207                self.expect_keyword_ident("to")?;
6208            }
6209            let new = self.expect_ident_like()?;
6210            return Ok(Statement::AlterSequence(
6211                crate::ast::AlterSequenceStatement {
6212                    name,
6213                    if_exists,
6214                    options: crate::ast::SequenceOptions::default(),
6215                    rename_to: Some(new),
6216                },
6217            ));
6218        }
6219        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6220        Ok(Statement::AlterSequence(
6221            crate::ast::AlterSequenceStatement {
6222                name,
6223                if_exists,
6224                options,
6225                rename_to: None,
6226            },
6227        ))
6228    }
6229
6230    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6231        let kw = self.expect_ident_like()?;
6232        match kw.to_ascii_lowercase().as_str() {
6233            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6234            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6235            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6236            other => Err(self.err(alloc::format!(
6237                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6238            ))),
6239        }
6240    }
6241
6242    fn parse_sequence_options(
6243        &mut self,
6244        allow_restart: bool,
6245    ) -> Result<crate::ast::SequenceOptions, ParseError> {
6246        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6247        let mut opts = SequenceOptions::default();
6248        #[allow(clippy::while_let_loop)]
6249        loop {
6250            // Match an ident; stop at any non-ident token (sentinel,
6251            // semicolon, end of statement).
6252            let kw_lc = match self.peek() {
6253                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6254                _ => break,
6255            };
6256            match kw_lc.as_str() {
6257                "increment" => {
6258                    self.advance();
6259                    // Optional BY.
6260                    if self.peek_is_by() {
6261                        self.advance();
6262                    }
6263                    opts.increment = Some(self.expect_signed_int()?);
6264                }
6265                "minvalue" => {
6266                    self.advance();
6267                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6268                }
6269                "maxvalue" => {
6270                    self.advance();
6271                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6272                }
6273                "no" => {
6274                    self.advance();
6275                    let what = self.expect_ident_like()?;
6276                    match what.to_ascii_lowercase().as_str() {
6277                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6278                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6279                        "cycle" => opts.cycle = Some(false),
6280                        other => {
6281                            return Err(self.err(alloc::format!(
6282                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6283                            )));
6284                        }
6285                    }
6286                }
6287                "start" => {
6288                    self.advance();
6289                    // Optional WITH.
6290                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6291                        if s.eq_ignore_ascii_case("with"))
6292                    {
6293                        self.advance();
6294                    }
6295                    opts.start = Some(self.expect_signed_int()?);
6296                }
6297                "restart" if allow_restart => {
6298                    self.advance();
6299                    // Optional WITH n; bare RESTART means restart at START.
6300                    let mut with_val: Option<i64> = None;
6301                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6302                        if s.eq_ignore_ascii_case("with"))
6303                    {
6304                        self.advance();
6305                        with_val = Some(self.expect_signed_int()?);
6306                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6307                        with_val = Some(self.expect_signed_int()?);
6308                    }
6309                    opts.restart = Some(with_val);
6310                }
6311                "cache" => {
6312                    self.advance();
6313                    opts.cache = Some(self.expect_signed_int()?);
6314                }
6315                "cycle" => {
6316                    self.advance();
6317                    opts.cycle = Some(true);
6318                }
6319                "owned" => {
6320                    self.advance();
6321                    match self.peek() {
6322                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6323                            self.advance();
6324                        }
6325                        other => {
6326                            return Err(
6327                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6328                            );
6329                        }
6330                    }
6331                    // OWNED BY {NONE | tab.col}. Read just one ident
6332                    // (NOT expect_ident_like which would auto-strip
6333                    // a schema prefix and consume the `.col` we need).
6334                    let first = match self.advance() {
6335                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6336                        other => {
6337                            return Err(self.err(alloc::format!(
6338                                "expected identifier or NONE after OWNED BY, got {other:?}"
6339                            )));
6340                        }
6341                    };
6342                    if first.eq_ignore_ascii_case("none") {
6343                        opts.owned_by = Some(SequenceOwnedBy::None);
6344                    } else if matches!(self.peek(), Token::Dot) {
6345                        self.advance();
6346                        let second = match self.advance() {
6347                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6348                            other => {
6349                                return Err(self.err(alloc::format!(
6350                                    "expected column name after OWNED BY {first}., got {other:?}"
6351                                )));
6352                            }
6353                        };
6354                        // v7.17 dump-compat fix — pg_dump emits
6355                        // OWNED BY clauses as
6356                        // `schema.table.column` (three segments).
6357                        // If a third `.<ident>` follows, treat the
6358                        // first ident as schema (drop it; SPG is
6359                        // single-schema) and the middle / last
6360                        // pair as table.column. Otherwise it's
6361                        // the two-segment form table.column.
6362                        if matches!(self.peek(), Token::Dot) {
6363                            self.advance();
6364                            let third = match self.advance() {
6365                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6366                                other => {
6367                                    return Err(self.err(alloc::format!(
6368                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6369                                    )));
6370                                }
6371                            };
6372                            let _ = first; // schema prefix discarded
6373                            opts.owned_by = Some(SequenceOwnedBy::Column {
6374                                table: second,
6375                                column: third,
6376                            });
6377                        } else {
6378                            opts.owned_by = Some(SequenceOwnedBy::Column {
6379                                table: first,
6380                                column: second,
6381                            });
6382                        }
6383                    } else {
6384                        return Err(self.err(alloc::format!(
6385                            "expected table.column or NONE after OWNED BY, got {first:?}"
6386                        )));
6387                    }
6388                }
6389                _ => break,
6390            }
6391        }
6392        Ok(opts)
6393    }
6394
6395    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6396        let neg = if matches!(self.peek(), Token::Minus) {
6397            self.advance();
6398            true
6399        } else {
6400            false
6401        };
6402        match self.peek() {
6403            Token::Integer(n) => {
6404                let v = *n;
6405                self.advance();
6406                Ok(if neg { -v } else { v })
6407            }
6408            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6409        }
6410    }
6411
6412    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6413    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6414    /// clause is fully accepted and discarded — SPG always runs
6415    /// constraint checks immediately (single-writer model). The
6416    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6417    /// in either order (per the SQL spec they're independent),
6418    /// though pg_dump always emits them in the canonical
6419    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6420    /// Stops at the first token that isn't part of the clause.
6421    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6422        self.consume_deferrable_clauses_timed().map(|_| ())
6423    }
6424
6425    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6426    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6427    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6428    /// NOT DEFERRABLE and a circular-FK migration could not load.
6429    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6430        let mut deferrable = false;
6431        let mut initially_deferred = false;
6432        loop {
6433            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6434            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6435                self.advance();
6436                deferrable = true;
6437                if self.consume_optional_initially_clause()? {
6438                    initially_deferred = true;
6439                }
6440                continue;
6441            }
6442            // `NOT DEFERRABLE` — already worked pre-3.1.
6443            if matches!(self.peek(), Token::Not) {
6444                let look = self.tokens.get(self.pos + 1);
6445                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6446                    self.advance(); // NOT
6447                    self.advance(); // DEFERRABLE
6448                    deferrable = false;
6449                    initially_deferred = false;
6450                    let _ = self.consume_optional_initially_clause()?;
6451                    continue;
6452                }
6453                break;
6454            }
6455            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6456            // accepts this without a leading [NOT] DEFERRABLE
6457            // (the timing keyword alone). pg_dump occasionally
6458            // emits it on FK constraints that inherit timing.
6459            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6460                if self.consume_optional_initially_clause()? {
6461                    initially_deferred = true;
6462                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6463                    deferrable = true;
6464                }
6465                continue;
6466            }
6467            break;
6468        }
6469        Ok((deferrable, initially_deferred))
6470    }
6471
6472    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6473    /// next token is `INITIALLY`, consume it plus the required
6474    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6475    /// Returns true when the timing seen was `DEFERRED`.
6476    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6477        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6478            return Ok(false);
6479        }
6480        self.advance(); // INITIALLY
6481        match self.advance() {
6482            Token::Ident(s)
6483                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6484            {
6485                Ok(s.eq_ignore_ascii_case("deferred"))
6486            }
6487            other => Err(self.err(alloc::format!(
6488                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6489            ))),
6490        }
6491    }
6492
6493    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6494    /// in its entirety so the parser returns Empty without
6495    /// touching the runtime. The CREATE+PROCEDURE keywords are
6496    /// already consumed; this swallows everything from the
6497    /// procedure name through the matching `END`, including
6498    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6499    /// (DELIMITER `//` makes the script splitter forward the
6500    /// whole block as one statement), `@var` session-variable
6501    /// references, and the trailing terminator.
6502    ///
6503    /// Tracks nesting depth so:
6504    ///   BEGIN
6505    ///     IF cond THEN
6506    ///       BEGIN ... END;
6507    ///     END IF;
6508    ///   END
6509    /// terminates at the outer END.
6510    fn consume_mysql_routine_body(&mut self) {
6511        // Outer skeleton: name, (...), optional clauses, BEGIN
6512        // <body> END [;]. Scan for the first BEGIN — anything
6513        // before it is signature decoration we don't care about.
6514        // Once inside BEGIN, count up on BEGIN, down on END.
6515        let mut depth: i32 = 0;
6516        let mut started = false;
6517        loop {
6518            match self.peek().clone() {
6519                Token::Begin => {
6520                    self.advance();
6521                    depth += 1;
6522                    started = true;
6523                }
6524                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6525                    self.advance();
6526                    if started {
6527                        depth -= 1;
6528                        if depth <= 0 {
6529                            // Optional trailing ident (`END IF`,
6530                            // `END LOOP`, `END WHILE`, `END CASE`,
6531                            // `END label_name`) — eat the next
6532                            // ident if present so we don't
6533                            // mistake `END IF;` for the outer
6534                            // close.
6535                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6536                                // If the next token is one of the
6537                                // PL/SQL block-closer keywords,
6538                                // the END belongs to an inner
6539                                // block; bump depth back up.
6540                                let is_inner_close = matches!(
6541                                    self.peek(),
6542                                    Token::Ident(s) | Token::QuotedIdent(s)
6543                                        if matches!(
6544                                            s.to_ascii_lowercase().as_str(),
6545                                            "if" | "loop" | "while" | "case" | "repeat"
6546                                        )
6547                                );
6548                                if is_inner_close {
6549                                    self.advance();
6550                                    depth += 1;
6551                                    continue;
6552                                }
6553                            }
6554                            // Eat optional trailing `;`.
6555                            if matches!(self.peek(), Token::Semicolon) {
6556                                self.advance();
6557                            }
6558                            return;
6559                        }
6560                    }
6561                }
6562                Token::Eof => return,
6563                _ => {
6564                    self.advance();
6565                }
6566            }
6567        }
6568    }
6569
6570    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6571    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6572    ///
6573    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6574    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6575    ///   ident, or `ident @ ident-or-quoted-string` host form)
6576    /// * `SQL SECURITY {DEFINER|INVOKER}`
6577    ///
6578    /// Each clause may appear at most once but in any order.
6579    /// The hints are pure planner / permission metadata that
6580    /// SPG's view-rewrite engine handles uniformly; we accept
6581    /// and discard. Returns `Ok(())` once a non-clause token is
6582    /// peeked (the caller then checks for the `VIEW` keyword).
6583    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6584        loop {
6585            match self.peek().clone() {
6586                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6587                    self.advance(); // ALGORITHM
6588                    // Optional `=`. MySQL spec requires it but be
6589                    // generous.
6590                    if matches!(self.peek(), Token::Eq) {
6591                        self.advance();
6592                    }
6593                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6594                    // bare ident; unknown values still parse so
6595                    // future MySQL versions don't break.
6596                    if matches!(
6597                        self.peek(),
6598                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6599                    ) {
6600                        self.advance();
6601                    }
6602                }
6603                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6604                    self.advance(); // DEFINER
6605                    if matches!(self.peek(), Token::Eq) {
6606                        self.advance();
6607                    }
6608                    // User: quoted string, ident, OR ident @ host
6609                    // (host may itself be quoted or bare).
6610                    match self.peek().clone() {
6611                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6612                            self.advance();
6613                            // Optional `@host`.
6614                            if matches!(self.peek(), Token::At) {
6615                                self.advance();
6616                                if matches!(
6617                                    self.peek(),
6618                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6619                                ) {
6620                                    self.advance();
6621                                }
6622                            }
6623                        }
6624                        _ => {}
6625                    }
6626                }
6627                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6628                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6629                    // when followed by SECURITY — the dispatcher must
6630                    // not consume a bare `SQL` token (it's not a
6631                    // legal CREATE prefix on its own).
6632                    let save = self.pos;
6633                    self.advance(); // SQL
6634                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6635                        if s2.eq_ignore_ascii_case("security"))
6636                    {
6637                        self.advance(); // SECURITY
6638                        // DEFINER / INVOKER trailing ident.
6639                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6640                            self.advance();
6641                        }
6642                    } else {
6643                        // Not a SQL SECURITY clause — roll back and
6644                        // bail; the caller will error out cleanly.
6645                        self.pos = save;
6646                        return Ok(());
6647                    }
6648                }
6649                _ => return Ok(()),
6650            }
6651        }
6652    }
6653
6654    fn parse_if_not_exists(&mut self) -> bool {
6655        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6656        {
6657            let save = self.pos;
6658            self.advance();
6659            if matches!(self.peek(), Token::Not) {
6660                self.advance();
6661                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6662                {
6663                    self.advance();
6664                    return true;
6665                }
6666            }
6667            self.pos = save;
6668        }
6669        false
6670    }
6671
6672    fn parse_if_exists(&mut self) -> bool {
6673        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6674        {
6675            let save = self.pos;
6676            self.advance();
6677            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6678            {
6679                self.advance();
6680                return true;
6681            }
6682            self.pos = save;
6683        }
6684        false
6685    }
6686
6687    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6688    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6689    /// been consumed.
6690    fn parse_create_trigger_after_keyword(
6691        &mut self,
6692        or_replace: bool,
6693    ) -> Result<Statement, ParseError> {
6694        let name = self.expect_ident_like()?;
6695        let timing = {
6696            let ident = self.expect_ident_like()?;
6697            if ident.eq_ignore_ascii_case("before") {
6698                TriggerTiming::Before
6699            } else if ident.eq_ignore_ascii_case("after") {
6700                TriggerTiming::After
6701            } else if ident.eq_ignore_ascii_case("instead") {
6702                let next = self.expect_ident_like()?;
6703                if !next.eq_ignore_ascii_case("of") {
6704                    return Err(self.err(alloc::format!(
6705                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6706                    )));
6707                }
6708                TriggerTiming::InsteadOf
6709            } else {
6710                return Err(self.err(alloc::format!(
6711                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6712                )));
6713            }
6714        };
6715        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6716        // OR is a reserved keyword token (Token::Or), not an Ident.
6717        // v7.13.0 — after an UPDATE event we may optionally see
6718        // `OF col, col, …` (mailrs round-5 G7). Columns are
6719        // captured into `update_columns` once across the whole
6720        // events list; multiple `UPDATE OF` clauses are rejected.
6721        let mut events: Vec<TriggerEvent> = Vec::new();
6722        let mut update_columns: Vec<String> = Vec::new();
6723        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6724        events.push(first_ev);
6725        if !first_cols.is_empty() {
6726            update_columns = first_cols;
6727        }
6728        while matches!(self.peek(), Token::Or) {
6729            self.advance();
6730            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6731            events.push(ev);
6732            if !cols.is_empty() {
6733                if !update_columns.is_empty() {
6734                    return Err(
6735                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6736                    );
6737                }
6738                update_columns = cols;
6739            }
6740        }
6741        // ON <table>
6742        let tok = self.peek();
6743        let Token::On = tok else {
6744            return Err(self.err(alloc::format!(
6745                "expected ON after trigger events, got {tok:?}"
6746            )));
6747        };
6748        self.advance();
6749        let table = self.expect_ident_like()?;
6750        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6751        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6752        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6753        // the trigger as a plain AFTER trigger (correct for every non-deferred
6754        // use; deferral timing is not yet honoured).
6755        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6756            if s.eq_ignore_ascii_case("from"))
6757        {
6758            self.advance();
6759            let _reftable = self.expect_ident_like()?;
6760        }
6761        self.consume_optional_deferrable_clauses()?;
6762        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6763        // keyword (Token::For); EACH / ROW / STATEMENT are bare
6764        // idents.
6765        if !matches!(self.peek(), Token::For) {
6766            return Err(self.err(alloc::format!(
6767                "expected FOR EACH ROW / STATEMENT, got {:?}",
6768                self.peek()
6769            )));
6770        }
6771        self.advance();
6772        let for_each = {
6773            let e = self.expect_ident_like()?;
6774            if !e.eq_ignore_ascii_case("each") {
6775                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6776            }
6777            let unit = self.expect_ident_like()?;
6778            if unit.eq_ignore_ascii_case("row") {
6779                TriggerForEach::Row
6780            } else if unit.eq_ignore_ascii_case("statement") {
6781                TriggerForEach::Statement
6782            } else {
6783                return Err(self.err(alloc::format!(
6784                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6785                )));
6786            }
6787        };
6788        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6789        let when_condition = if matches!(self.peek(),
6790            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6791        {
6792            self.advance();
6793            Some(self.parse_paren_expr("WHEN")?)
6794        } else {
6795            None
6796        };
6797        // EXECUTE FUNCTION/PROCEDURE name(...)
6798        let exec = self.expect_ident_like()?;
6799        if !exec.eq_ignore_ascii_case("execute") {
6800            return Err(self.err(alloc::format!(
6801                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6802            )));
6803        }
6804        let fn_or_proc = self.expect_ident_like()?;
6805        if !(fn_or_proc.eq_ignore_ascii_case("function")
6806            || fn_or_proc.eq_ignore_ascii_case("procedure"))
6807        {
6808            return Err(self.err(alloc::format!(
6809                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6810            )));
6811        }
6812        let function = self.expect_ident_like()?;
6813        // Optional empty arg list `()`.
6814        if matches!(self.peek(), Token::LParen) {
6815            self.advance();
6816            if !matches!(self.peek(), Token::RParen) {
6817                return Err(self.err(alloc::format!(
6818                    "v7.12.4 trigger function calls take no args; got {:?}",
6819                    self.peek()
6820                )));
6821            }
6822            self.advance();
6823        }
6824        Ok(Statement::CreateTrigger(CreateTriggerStatement {
6825            name,
6826            or_replace,
6827            timing,
6828            events,
6829            table,
6830            for_each,
6831            function,
6832            update_columns,
6833            when_condition,
6834        }))
6835    }
6836
6837    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6838    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6839    fn parse_create_rule_after_keyword(
6840        &mut self,
6841        or_replace: bool,
6842    ) -> Result<Statement, ParseError> {
6843        let name = self.expect_ident_like()?;
6844        if !matches!(self.peek(), Token::As) {
6845            return Err(self.err(alloc::format!(
6846                "expected AS in CREATE RULE, got {:?}",
6847                self.peek()
6848            )));
6849        }
6850        self.advance();
6851        if !matches!(self.peek(), Token::On) {
6852            return Err(self.err(alloc::format!(
6853                "expected ON in CREATE RULE, got {:?}",
6854                self.peek()
6855            )));
6856        }
6857        self.advance();
6858        let event = self.parse_rule_event()?;
6859        if !matches!(self.peek(), Token::To)
6860            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6861        {
6862            return Err(self.err(alloc::format!(
6863                "expected TO after rule event, got {:?}",
6864                self.peek()
6865            )));
6866        }
6867        self.advance();
6868        let table = self.expect_ident_like()?;
6869        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6870        let when_condition = if matches!(self.peek(), Token::Where) {
6871            self.advance();
6872            Some(self.parse_expr(0)?)
6873        } else {
6874            None
6875        };
6876        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6877        {
6878            return Err(self.err(alloc::format!(
6879                "expected DO in CREATE RULE, got {:?}",
6880                self.peek()
6881            )));
6882        }
6883        self.advance();
6884        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6885        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6886        {
6887            self.advance();
6888            true
6889        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6890            self.advance();
6891            false
6892        } else {
6893            false
6894        };
6895        // `NOTHING` | `( cmd; … )` | `cmd`.
6896        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6897        {
6898            self.advance();
6899            Vec::new()
6900        } else if matches!(self.peek(), Token::LParen) {
6901            self.advance();
6902            let mut cmds = Vec::new();
6903            loop {
6904                cmds.push(self.parse_one_statement()?);
6905                if matches!(self.peek(), Token::Semicolon) {
6906                    self.advance();
6907                    if matches!(self.peek(), Token::RParen) {
6908                        break;
6909                    }
6910                    continue;
6911                }
6912                break;
6913            }
6914            if !matches!(self.peek(), Token::RParen) {
6915                return Err(self.err(alloc::format!(
6916                    "expected ) closing the CREATE RULE command list, got {:?}",
6917                    self.peek()
6918                )));
6919            }
6920            self.advance();
6921            cmds
6922        } else {
6923            alloc::vec![self.parse_one_statement()?]
6924        };
6925        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6926            name,
6927            or_replace,
6928            event,
6929            table,
6930            instead,
6931            when_condition,
6932            commands,
6933        }))
6934    }
6935
6936    /// v7.39 (round 139) — a rule event keyword → uppercase string.
6937    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6938        if matches!(self.peek(), Token::Insert) {
6939            self.advance();
6940            return Ok(alloc::string::String::from("INSERT"));
6941        }
6942        if matches!(self.peek(), Token::Select) {
6943            self.advance();
6944            return Ok(alloc::string::String::from("SELECT"));
6945        }
6946        match self.peek() {
6947            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6948                self.advance();
6949                Ok(alloc::string::String::from("UPDATE"))
6950            }
6951            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6952                self.advance();
6953                Ok(alloc::string::String::from("DELETE"))
6954            }
6955            other => Err(self.err(alloc::format!(
6956                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6957            ))),
6958        }
6959    }
6960
6961    /// v7.13.0 — parse one trigger event, then optionally consume
6962    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6963    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6964    fn parse_trigger_event_with_optional_of(
6965        &mut self,
6966    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6967        let ev = self.parse_trigger_event()?;
6968        if !matches!(ev, TriggerEvent::Update) {
6969            return Ok((ev, Vec::new()));
6970        }
6971        // `OF` is a bare ident.
6972        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6973            return Ok((ev, Vec::new()));
6974        }
6975        self.advance(); // OF
6976        let mut cols: Vec<String> = Vec::new();
6977        loop {
6978            cols.push(self.expect_ident_like()?);
6979            if matches!(self.peek(), Token::Comma) {
6980                self.advance();
6981                continue;
6982            }
6983            break;
6984        }
6985        if cols.is_empty() {
6986            return Err(
6987                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6988            );
6989        }
6990        Ok((ev, cols))
6991    }
6992
6993    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6994    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6995    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6996    /// inside the body.
6997    /// Called by [`parse_plpgsql_body`] after the body's tokens
6998    /// have been lexed into this temporary parser.
6999    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7000        // v7.12.6 — optional DECLARE prelude.
7001        let declarations = if matches!(
7002            self.peek(),
7003            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7004        ) {
7005            self.advance();
7006            self.parse_plpgsql_declare_block()?
7007        } else {
7008            Vec::new()
7009        };
7010        // BEGIN keyword (PL/pgSQL — distinct from the SQL
7011        // `BEGIN` transaction-start, but we can reuse the
7012        // reserved Token::Begin since the body is a separate
7013        // lex/parse context).
7014        if !matches!(self.peek(), Token::Begin) {
7015            return Err(self.err(alloc::format!(
7016                "expected BEGIN at start of plpgsql block, got {:?}",
7017                self.peek()
7018            )));
7019        }
7020        self.advance();
7021        let statements = self.parse_plpgsql_stmt_list_until_end()?;
7022        // v7.37.20 (20.10) — optional EXCEPTION clause between the
7023        // body's last statement and the trailing END. When present
7024        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7025        // arms terminated by END.
7026        let exception_handlers = if matches!(
7027            self.peek(),
7028            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7029        ) {
7030            self.advance();
7031            self.parse_plpgsql_exception_handlers()?
7032        } else {
7033            Vec::new()
7034        };
7035        Ok(PlPgSqlBlock {
7036            declarations,
7037            statements,
7038            exception_handlers,
7039        })
7040    }
7041
7042    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7043    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7044    fn parse_plpgsql_exception_handlers(
7045        &mut self,
7046    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7047        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7048        loop {
7049            // Stop at END — the block-level trailing END LOOP / END;
7050            // is handled by the caller.
7051            if matches!(
7052                self.peek(),
7053                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7054            ) {
7055                return Ok(out);
7056            }
7057            // WHEN <cond> [OR <cond>]* THEN <body>
7058            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7059            {
7060                return Err(self.err(alloc::format!(
7061                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
7062                    self.peek()
7063                )));
7064            }
7065            self.advance();
7066            let mut conditions: Vec<String> = Vec::new();
7067            conditions.push(self.expect_ident_like()?);
7068            while matches!(self.peek(), Token::Or) {
7069                self.advance();
7070                conditions.push(self.expect_ident_like()?);
7071            }
7072            let then_kw = self.expect_ident_like()?;
7073            if !then_kw.eq_ignore_ascii_case("then") {
7074                return Err(self.err(alloc::format!(
7075                    "expected THEN after WHEN condition list, got {then_kw:?}"
7076                )));
7077            }
7078            let body = self.parse_plpgsql_stmt_list_until_end()?;
7079            out.push(crate::ast::ExceptionHandler { conditions, body });
7080        }
7081    }
7082
7083    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7084    /// prelude. Caller has already consumed `DECLARE`. We stop
7085    /// reading entries when we hit `BEGIN`.
7086    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7087        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7088        loop {
7089            if matches!(self.peek(), Token::Begin) {
7090                return Ok(out);
7091            }
7092            let name = self.expect_ident_like()?;
7093            // v7.37.20 (20.7) — type inference: if the next token is
7094            // `:=` or `=` (no explicit type), infer from the default
7095            // expression. Otherwise the ident that follows is the
7096            // declared type.
7097            //
7098            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7099            // (PG-standard). SPG parse-accepts and treats identically
7100            // to inference — the eventual runtime value determines
7101            // the local's type, which is faithful to how SPG handles
7102            // untyped locals today (see 20.7). Full compile-time
7103            // catalog lookup queues with v7.40 PL/pgSQL epic.
7104            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7105                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7106                // downstream declaration walker to type the local by
7107                // the runtime type of the default expression.
7108                FunctionArgType::Raw("_infer_".into())
7109            } else {
7110                let ty_token = self.expect_ident_like()?;
7111                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7112                // consume optional `.<ident>` qualifier + `%<KW>`
7113                // suffix. Both qualifier and suffix map to _infer_.
7114                if matches!(self.peek(), Token::Dot) {
7115                    self.advance();
7116                    let _ = self.expect_ident_like()?;
7117                }
7118                if matches!(self.peek(), Token::Percent) {
7119                    self.advance();
7120                    // Consume the trailing TYPE / ROWTYPE ident.
7121                    let _ = self.expect_ident_like()?;
7122                    FunctionArgType::Raw("_infer_".into())
7123                } else {
7124                    match map_type_ident_to_column_type_name(&ty_token) {
7125                        Some(t) => FunctionArgType::Typed(t),
7126                        None => FunctionArgType::Raw(ty_token),
7127                    }
7128                }
7129            };
7130            let default = match self.peek() {
7131                Token::ColonEq => {
7132                    self.advance();
7133                    Some(self.parse_expr(0)?)
7134                }
7135                Token::Eq => {
7136                    // PL/pgSQL also accepts `=` for the
7137                    // DECLARE default (PG treats them the same
7138                    // in this position).
7139                    self.advance();
7140                    Some(self.parse_expr(0)?)
7141                }
7142                _ => None,
7143            };
7144            // Mandatory `;` between declarations.
7145            if !matches!(self.peek(), Token::Semicolon) {
7146                return Err(self.err(alloc::format!(
7147                    "expected ; after DECLARE entry for {name:?}, got {:?}",
7148                    self.peek()
7149                )));
7150            }
7151            self.advance();
7152            out.push(PlPgSqlDeclare { name, ty, default });
7153        }
7154    }
7155
7156    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7157    /// the terminating `END;` (or `END IF;` etc — handled by the
7158    /// per-construct sub-parsers). Used by both the outer block
7159    /// and the IF/ELSE branch bodies.
7160    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7161        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7162        loop {
7163            // Allow trailing semicolons + END.
7164            while matches!(self.peek(), Token::Semicolon) {
7165                self.advance();
7166            }
7167            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7168            if matches!(
7169                self.peek(),
7170                Token::Ident(s) | Token::QuotedIdent(s)
7171                    if s.eq_ignore_ascii_case("end")
7172                        || s.eq_ignore_ascii_case("else")
7173                        || s.eq_ignore_ascii_case("elsif")
7174                        || s.eq_ignore_ascii_case("elseif")
7175                        || s.eq_ignore_ascii_case("exception")
7176                        || s.eq_ignore_ascii_case("when")
7177            ) {
7178                return Ok(statements);
7179            }
7180            // Otherwise: one statement, then expect `;` or
7181            // a block-terminator keyword.
7182            let stmt = self.parse_plpgsql_stmt()?;
7183            statements.push(stmt);
7184            match self.peek() {
7185                Token::Semicolon => {
7186                    self.advance();
7187                }
7188                Token::Ident(s) | Token::QuotedIdent(s)
7189                    if s.eq_ignore_ascii_case("end")
7190                        || s.eq_ignore_ascii_case("else")
7191                        || s.eq_ignore_ascii_case("elsif")
7192                        || s.eq_ignore_ascii_case("elseif")
7193                        || s.eq_ignore_ascii_case("exception")
7194                        || s.eq_ignore_ascii_case("when") =>
7195                {
7196                    // Final statement of the block without `;`.
7197                }
7198                other => {
7199                    return Err(self.err(alloc::format!(
7200                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7201                    )));
7202                }
7203            }
7204        }
7205    }
7206
7207    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7208        // RETURN keyword?
7209        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7210        {
7211            self.advance();
7212            return self.parse_plpgsql_return();
7213        }
7214        // v7.12.6 — IF block.
7215        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7216        {
7217            self.advance();
7218            return self.parse_plpgsql_if();
7219        }
7220        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7221        // Detected by peeking that token pos+3 is Ident("execute").
7222        if matches!(self.peek(), Token::For)
7223            && matches!(
7224                self.tokens.get(self.pos + 1),
7225                Some(Token::Ident(_) | Token::QuotedIdent(_))
7226            )
7227            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7228            && matches!(
7229                self.tokens.get(self.pos + 3),
7230                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7231            )
7232        {
7233            self.advance(); // FOR
7234            let var = self.expect_ident_like()?;
7235            self.advance(); // IN
7236            self.advance(); // EXECUTE
7237            // Prescan for LOOP at paren depth 0 so parse_expr stops
7238            // before the LOOP keyword (same trick as the bare-SELECT
7239            // ForQuery arm).
7240            let mut depth: i32 = 0;
7241            let mut loop_pos: Option<usize> = None;
7242            let mut scan = self.pos;
7243            while scan < self.tokens.len() {
7244                match self.tokens.get(scan) {
7245                    Some(Token::LParen) => depth += 1,
7246                    Some(Token::RParen) => depth -= 1,
7247                    Some(Token::Ident(s) | Token::QuotedIdent(s))
7248                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7249                    {
7250                        loop_pos = Some(scan);
7251                        break;
7252                    }
7253                    _ => {}
7254                }
7255                scan += 1;
7256            }
7257            let loop_pos = loop_pos.ok_or_else(|| {
7258                self.err(alloc::format!(
7259                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7260                ))
7261            })?;
7262            let saved_loop = self.tokens[loop_pos].clone();
7263            self.tokens[loop_pos] = Token::Semicolon;
7264            let expr_result = self.parse_expr(0);
7265            self.tokens[loop_pos] = saved_loop;
7266            let sql_expr = expr_result?;
7267            let loop_kw = self.expect_ident_like()?;
7268            if !loop_kw.eq_ignore_ascii_case("loop") {
7269                return Err(self.err(alloc::format!(
7270                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7271                )));
7272            }
7273            let body = self.parse_plpgsql_stmt_list_until_end()?;
7274            let end_kw = self.expect_ident_like()?;
7275            if !end_kw.eq_ignore_ascii_case("end") {
7276                return Err(self.err(alloc::format!(
7277                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7278                )));
7279            }
7280            let loop_kw2 = self.expect_ident_like()?;
7281            if !loop_kw2.eq_ignore_ascii_case("loop") {
7282                return Err(self.err(alloc::format!(
7283                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7284                )));
7285            }
7286            return Ok(PlPgSqlStmt::ForExecute {
7287                var,
7288                sql_expr,
7289                body,
7290            });
7291        }
7292        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7293        //
7294        // Two syntactic forms:
7295        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7296        //   FOR var IN (SELECT ...) LOOP ...
7297        //
7298        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7299        // the trailing `LOOP` keyword as a table alias, we prescan
7300        // forward to find LOOP at paren depth 0, splice a fake
7301        // Semicolon at that position (so SELECT parses cleanly),
7302        // then re-splice LOOP back in.
7303        //
7304        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7305        // LOOP directly — no scan required.
7306        if matches!(self.peek(), Token::For)
7307            && matches!(
7308                self.tokens.get(self.pos + 1),
7309                Some(Token::Ident(_) | Token::QuotedIdent(_))
7310            )
7311            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7312            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7313                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7314        {
7315            self.advance(); // FOR
7316            let var = self.expect_ident_like()?;
7317            // IN
7318            self.advance();
7319            let query = if matches!(self.peek(), Token::LParen) {
7320                // Paren-wrapped SELECT.
7321                self.advance();
7322                let inner = self.parse_select_stmt()?;
7323                let Statement::Select(q) = inner else {
7324                    return Err(self.err(alloc::format!(
7325                        "expected SELECT inside (…), got {:?}",
7326                        self.peek()
7327                    )));
7328                };
7329                if !matches!(self.peek(), Token::RParen) {
7330                    return Err(self.err(alloc::format!(
7331                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7332                        self.peek()
7333                    )));
7334                }
7335                self.advance();
7336                q
7337            } else {
7338                // Bare SELECT: prescan to find the LOOP boundary.
7339                let mut depth: i32 = 0;
7340                let mut loop_pos: Option<usize> = None;
7341                let mut scan = self.pos;
7342                while scan < self.tokens.len() {
7343                    match self.tokens.get(scan) {
7344                        Some(Token::LParen) => depth += 1,
7345                        Some(Token::RParen) => depth -= 1,
7346                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7347                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7348                        {
7349                            loop_pos = Some(scan);
7350                            break;
7351                        }
7352                        _ => {}
7353                    }
7354                    scan += 1;
7355                }
7356                let loop_pos = loop_pos.ok_or_else(|| {
7357                    self.err(alloc::format!(
7358                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7359                    ))
7360                })?;
7361                // Swap the LOOP token with a synthetic Semicolon so
7362                // parse_select_stmt stops there, then restore afterward.
7363                let saved_loop = self.tokens[loop_pos].clone();
7364                self.tokens[loop_pos] = Token::Semicolon;
7365                let parse_result = self.parse_select_stmt();
7366                self.tokens[loop_pos] = saved_loop;
7367                let inner = parse_result?;
7368                let Statement::Select(q) = inner else {
7369                    return Err(self.err(alloc::format!(
7370                        "expected SELECT after FOR <var> IN, got {:?}",
7371                        self.peek()
7372                    )));
7373                };
7374                q
7375            };
7376            let loop_kw = self.expect_ident_like()?;
7377            if !loop_kw.eq_ignore_ascii_case("loop") {
7378                return Err(self.err(alloc::format!(
7379                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7380                )));
7381            }
7382            let body = self.parse_plpgsql_stmt_list_until_end()?;
7383            let end_kw = self.expect_ident_like()?;
7384            if !end_kw.eq_ignore_ascii_case("end") {
7385                return Err(self.err(alloc::format!(
7386                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7387                )));
7388            }
7389            let loop_kw2 = self.expect_ident_like()?;
7390            if !loop_kw2.eq_ignore_ascii_case("loop") {
7391                return Err(self.err(alloc::format!(
7392                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7393                )));
7394            }
7395            return Ok(PlPgSqlStmt::ForQuery {
7396                var,
7397                query: Box::new(query),
7398                body,
7399            });
7400        }
7401        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7402        // FOR is a reserved keyword token (Token::For).
7403        if matches!(self.peek(), Token::For)
7404            && matches!(
7405                self.tokens.get(self.pos + 1),
7406                Some(Token::Ident(_) | Token::QuotedIdent(_))
7407            )
7408            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7409        {
7410            self.advance(); // FOR
7411            let var = self.expect_ident_like()?;
7412            if !matches!(self.peek(), Token::In) {
7413                return Err(self.err(alloc::format!(
7414                    "expected IN after FOR <var>, got {:?}",
7415                    self.peek()
7416                )));
7417            }
7418            self.advance();
7419            let reverse = matches!(
7420                self.peek(),
7421                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7422            );
7423            if reverse {
7424                self.advance();
7425            }
7426            let start = self.parse_expr(0)?;
7427            if !matches!(self.peek(), Token::DotDot) {
7428                return Err(self.err(alloc::format!(
7429                    "expected '..' between FOR loop bounds, got {:?}",
7430                    self.peek()
7431                )));
7432            }
7433            self.advance();
7434            let end = self.parse_expr(0)?;
7435            let loop_kw = self.expect_ident_like()?;
7436            if !loop_kw.eq_ignore_ascii_case("loop") {
7437                return Err(self.err(alloc::format!(
7438                    "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7439                )));
7440            }
7441            let body = self.parse_plpgsql_stmt_list_until_end()?;
7442            let end_kw = self.expect_ident_like()?;
7443            if !end_kw.eq_ignore_ascii_case("end") {
7444                return Err(self.err(alloc::format!(
7445                    "expected END LOOP after FOR body, got {end_kw:?}"
7446                )));
7447            }
7448            let loop_kw2 = self.expect_ident_like()?;
7449            if !loop_kw2.eq_ignore_ascii_case("loop") {
7450                return Err(self.err(alloc::format!(
7451                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7452                )));
7453            }
7454            return Ok(PlPgSqlStmt::ForRange {
7455                var,
7456                start,
7457                end,
7458                reverse,
7459                body,
7460            });
7461        }
7462        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7463        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7464        {
7465            self.advance();
7466            let body = self.parse_plpgsql_stmt_list_until_end()?;
7467            let end_kw = self.expect_ident_like()?;
7468            if !end_kw.eq_ignore_ascii_case("end") {
7469                return Err(self.err(alloc::format!(
7470                    "expected END LOOP after LOOP body, got {end_kw:?}"
7471                )));
7472            }
7473            let loop_kw = self.expect_ident_like()?;
7474            if !loop_kw.eq_ignore_ascii_case("loop") {
7475                return Err(self.err(alloc::format!(
7476                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7477                )));
7478            }
7479            return Ok(PlPgSqlStmt::Loop { body });
7480        }
7481        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7482        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7483        {
7484            self.advance();
7485            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7486            {
7487                self.advance();
7488                Some(self.parse_expr(0)?)
7489            } else {
7490                None
7491            };
7492            return Ok(PlPgSqlStmt::Exit { when });
7493        }
7494        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7495        // already-parsed Statement or a runtime-computed SQL string.
7496        // The disambiguator vs the extended-query-protocol `EXECUTE
7497        // <stmt_name>` (which is a top-level Statement, not a
7498        // plpgsql line) is that inside a DO block / trigger body the
7499        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7500        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7501        {
7502            self.advance();
7503            let sql = self.parse_expr(0)?;
7504            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7505        }
7506        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7507        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7508        {
7509            self.advance();
7510            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7511            {
7512                self.advance();
7513                Some(self.parse_expr(0)?)
7514            } else {
7515                None
7516            };
7517            return Ok(PlPgSqlStmt::Continue { when });
7518        }
7519        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7520        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7521        {
7522            self.advance();
7523            let condition = self.parse_expr(0)?;
7524            let loop_kw = self.expect_ident_like()?;
7525            if !loop_kw.eq_ignore_ascii_case("loop") {
7526                return Err(self.err(alloc::format!(
7527                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7528                )));
7529            }
7530            let body = self.parse_plpgsql_stmt_list_until_end()?;
7531            // Expect END LOOP.
7532            let end_kw = self.expect_ident_like()?;
7533            if !end_kw.eq_ignore_ascii_case("end") {
7534                return Err(self.err(alloc::format!(
7535                    "expected END LOOP after WHILE body, got {end_kw:?}"
7536                )));
7537            }
7538            let loop_kw2 = self.expect_ident_like()?;
7539            if !loop_kw2.eq_ignore_ascii_case("loop") {
7540                return Err(self.err(alloc::format!(
7541                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7542                )));
7543            }
7544            return Ok(PlPgSqlStmt::While { condition, body });
7545        }
7546        // v7.12.6 — RAISE.
7547        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7548        {
7549            self.advance();
7550            return self.parse_plpgsql_raise();
7551        }
7552        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7553        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7554        {
7555            self.advance();
7556            let condition = self.parse_expr(0)?;
7557            let message = if matches!(self.peek(), Token::Comma) {
7558                self.advance();
7559                Some(self.parse_expr(0)?)
7560            } else {
7561                None
7562            };
7563            return Ok(PlPgSqlStmt::Assert { condition, message });
7564        }
7565        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7566        //   "PERFORM is equivalent to SELECT but discards the
7567        //    result." Side effects (function calls, RAISE inside
7568        //    SQL functions, etc.) still execute. We desugar to
7569        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7570        //    existing embedded-statement path handles execution +
7571        //    result-discard cleanly. The result is naturally
7572        //    discarded because EmbeddedSql doesn't propagate row
7573        //    sets back to the plpgsql interpreter.
7574        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7575        {
7576            self.advance();
7577            // Splice a synthetic Token::Select into the stream at
7578            // the current position so parse_select_stmt parses the
7579            // remainder as a normal SELECT body. Token-stream
7580            // surgery mirrors the try_parse_plpgsql_select_into
7581            // pattern used for SELECT … INTO desugaring.
7582            self.tokens.insert(self.pos, Token::Select);
7583            let select = self.parse_select_stmt()?;
7584            let Statement::Select(s) = select else {
7585                return Err(self.err(alloc::format!(
7586                    "expected SELECT body after PERFORM, got {:?}",
7587                    self.peek()
7588                )));
7589            };
7590            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7591        }
7592        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7593        // plpgsql-specific shape (mailrs round-10 migrate-042).
7594        // PG's SELECT INTO at top-level SQL would CREATE a new
7595        // table; inside plpgsql it ASSIGNS the query result to
7596        // a local variable. We detect the INTO at paren-depth
7597        // 0 between SELECT and the statement boundary; if
7598        // found, split the token stream into "pre-INTO
7599        // projection" + "var" + "post-INTO FROM/WHERE…" and
7600        // rebuild as a SelectInto with a regular SELECT body
7601        // (no INTO clause).
7602        if matches!(self.peek(), Token::Select)
7603            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7604        {
7605            return Ok(PlPgSqlStmt::SelectInto {
7606                var: var_name,
7607                body: Box::new(select_body),
7608            });
7609        }
7610        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7611        // SELECT can appear directly inside a trigger body; we
7612        // recurse into the regular Statement parser, which will
7613        // stop at the trailing `;` (which our caller then
7614        // consumes).
7615        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7616        // also embed ALTER / CREATE / DROP statements; route
7617        // those through the same parser so the DO body parses
7618        // cleanly.
7619        if matches!(self.peek(), Token::Insert)
7620            || matches!(self.peek(), Token::Select)
7621            || matches!(self.peek(), Token::Create)
7622            || matches!(self.peek(), Token::Drop)
7623            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7624                if s.eq_ignore_ascii_case("update")
7625                    || s.eq_ignore_ascii_case("delete")
7626                    || s.eq_ignore_ascii_case("alter"))
7627        {
7628            let stmt = self.parse_one_statement()?;
7629            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7630        }
7631        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7632        // followed by `:=` and an expression.
7633        let target = self.parse_plpgsql_assign_target()?;
7634        // PL/pgSQL assignment uses `:=`. The lexer represents
7635        // this as a colon followed by `=`; check both shapes.
7636        match self.peek() {
7637            Token::ColonEq => {
7638                self.advance();
7639            }
7640            Token::Colon => {
7641                self.advance();
7642                if !matches!(self.peek(), Token::Eq) {
7643                    return Err(self.err(alloc::format!(
7644                        "expected := after plpgsql assign target, got `:` then {:?}",
7645                        self.peek()
7646                    )));
7647                }
7648                self.advance();
7649            }
7650            other => {
7651                return Err(self.err(alloc::format!(
7652                    "expected := after plpgsql assign target, got {other:?}"
7653                )));
7654            }
7655        }
7656        let value = self.parse_expr(0)?;
7657        Ok(PlPgSqlStmt::Assign { target, value })
7658    }
7659
7660    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7661    /// [ELSE body] END IF`. `IF` keyword already consumed.
7662    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7663        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7664        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7665        loop {
7666            // <expr> THEN
7667            let cond = self.parse_expr(0)?;
7668            let then_kw = self.expect_ident_like()?;
7669            if !then_kw.eq_ignore_ascii_case("then") {
7670                return Err(self.err(alloc::format!(
7671                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7672                )));
7673            }
7674            let body = self.parse_plpgsql_stmt_list_until_end()?;
7675            branches.push((cond, body));
7676            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7677            match self.peek() {
7678                Token::Ident(s) | Token::QuotedIdent(s)
7679                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7680                {
7681                    self.advance();
7682                    continue;
7683                }
7684                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7685                    self.advance();
7686                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7687                    break;
7688                }
7689                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7690                    break;
7691                }
7692                other => {
7693                    return Err(self.err(alloc::format!(
7694                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7695                    )));
7696                }
7697            }
7698        }
7699        // Expect `END IF` (the END keyword is the one we're
7700        // looking at right now).
7701        let end_kw = self.expect_ident_like()?;
7702        if !end_kw.eq_ignore_ascii_case("end") {
7703            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7704        }
7705        let if_kw = self.expect_ident_like()?;
7706        if !if_kw.eq_ignore_ascii_case("if") {
7707            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7708        }
7709        Ok(PlPgSqlStmt::If {
7710            branches,
7711            else_branch,
7712        })
7713    }
7714
7715    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7716    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7717    /// is already consumed.
7718    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7719        let lvl_ident = self.expect_ident_like()?;
7720        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7721            "notice" => RaiseLevel::Notice,
7722            "warning" => RaiseLevel::Warning,
7723            "info" => RaiseLevel::Info,
7724            "log" => RaiseLevel::Log,
7725            "debug" => RaiseLevel::Debug,
7726            "exception" => RaiseLevel::Exception,
7727            other => {
7728                return Err(self.err(alloc::format!(
7729                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7730                )));
7731            }
7732        };
7733        // Message: required for v7.12.6. PG accepts a bare
7734        // RAISE-rethrow form (no message), reserved for future
7735        // RAISE-no-args support.
7736        let Token::String(msg) = self.peek() else {
7737            return Err(self.err(alloc::format!(
7738                "expected RAISE message string, got {:?}",
7739                self.peek()
7740            )));
7741        };
7742        let message = msg.clone();
7743        self.advance();
7744        // Optional comma-separated args (PG `%` format substitution).
7745        let mut args: Vec<Expr> = Vec::new();
7746        while matches!(self.peek(), Token::Comma) {
7747            self.advance();
7748            args.push(self.parse_expr(0)?);
7749        }
7750        Ok(PlPgSqlStmt::Raise {
7751            level,
7752            message,
7753            args,
7754        })
7755    }
7756
7757    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7758    /// <projection> INTO <var> [FROM …]` (mailrs round-10
7759    /// migrate-042). Returns `(rebuilt_select_without_into,
7760    /// var_name)` when the pattern matches; `None` for
7761    /// regular SELECTs (those go through the embedded-SQL
7762    /// path). Token-stream surgery so the rebuilt SELECT
7763    /// parses through the regular `parse_select_stmt`.
7764    #[allow(clippy::too_many_lines)]
7765    fn try_parse_plpgsql_select_into(
7766        &mut self,
7767    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7768        // Scan forward from `self.pos + 1` (past Token::Select)
7769        // for Token::Into at paren-depth 0, stopping at the
7770        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7771        // end the plpgsql statement.
7772        let start = self.pos;
7773        let mut into_pos: Option<usize> = None;
7774        let mut depth: i32 = 0;
7775        let mut i = start + 1;
7776        while i < self.tokens.len() {
7777            match &self.tokens[i] {
7778                Token::LParen => depth += 1,
7779                Token::RParen => depth -= 1,
7780                Token::Semicolon if depth == 0 => break,
7781                Token::Ident(s)
7782                    if depth == 0
7783                        && (s.eq_ignore_ascii_case("end")
7784                            || s.eq_ignore_ascii_case("else")
7785                            || s.eq_ignore_ascii_case("elsif")) =>
7786                {
7787                    break;
7788                }
7789                Token::Into if depth == 0 => {
7790                    into_pos = Some(i);
7791                    break;
7792                }
7793                _ => {}
7794            }
7795            i += 1;
7796        }
7797        let Some(into_at) = into_pos else {
7798            return Ok(None);
7799        };
7800        // The token immediately after INTO must be the target
7801        // var ident; anything else (e.g. INSERT INTO table)
7802        // ruled out by the depth-0 check above. Capture it.
7803        let var = match self.tokens.get(into_at + 1) {
7804            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7805            other => {
7806                return Err(self.err(alloc::format!(
7807                    "expected variable name after SELECT … INTO, got {other:?}"
7808                )));
7809            }
7810        };
7811        // Find the end of the plpgsql SELECT INTO statement —
7812        // same boundary rules as the depth-0 scan above.
7813        let mut end = into_at + 2;
7814        let mut depth2: i32 = 0;
7815        while end < self.tokens.len() {
7816            match &self.tokens[end] {
7817                Token::LParen => depth2 += 1,
7818                Token::RParen => depth2 -= 1,
7819                Token::Semicolon if depth2 == 0 => break,
7820                Token::Ident(s)
7821                    if depth2 == 0
7822                        && (s.eq_ignore_ascii_case("end")
7823                            || s.eq_ignore_ascii_case("else")
7824                            || s.eq_ignore_ascii_case("elsif")) =>
7825                {
7826                    break;
7827                }
7828                _ => {}
7829            }
7830            end += 1;
7831        }
7832        // Rebuild a token stream that represents the SELECT
7833        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7834        // post-var tokens up to statement end]. Run the
7835        // regular `parse_select_stmt` against it.
7836        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7837        for j in start..into_at {
7838            rebuilt.push(self.tokens[j].clone());
7839        }
7840        for j in (into_at + 2)..end {
7841            rebuilt.push(self.tokens[j].clone());
7842        }
7843        rebuilt.push(Token::Eof);
7844        let saved_pos = self.pos;
7845        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7846        self.pos = 0;
7847        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7848        if !matches!(self.peek(), Token::Select) {
7849            self.tokens = saved_tokens;
7850            self.pos = saved_pos;
7851            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7852        }
7853        let sel = self.parse_select_stmt();
7854        self.tokens = saved_tokens;
7855        self.pos = end;
7856        let sel = sel?;
7857        let Statement::Select(body) = sel else {
7858            return Err(self.err(alloc::format!(
7859                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7860            )));
7861        };
7862        Ok(Some((body, var)))
7863    }
7864
7865    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7866        // v7.16.1 — read the head token DIRECTLY rather than
7867        // via `expect_ident_like`. The v7.14.0 schema-qualifier
7868        // strip (`public.t` → `t`) inside `expect_ident_like`
7869        // greedily consumes any `ident . ident` pair, which
7870        // silently turned every `NEW.col := …` /
7871        // `OLD.col := …` plpgsql assignment into a Local("col")
7872        // assignment — the head "new"/"old" was eaten as if it
7873        // were a schema name and the Dot was consumed too, so
7874        // this function's own `peek() == Token::Dot` check
7875        // below never fired. Every BEFORE trigger that rewrote
7876        // a NEW cell was a silent no-op for two major releases
7877        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7878        // gate failures were investigated as v7.16.1 backlog.
7879        let head = match self.advance() {
7880            Token::Ident(s) | Token::QuotedIdent(s) => s,
7881            other => {
7882                return Err(self.err(alloc::format!(
7883                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7884                )));
7885            }
7886        };
7887        if matches!(self.peek(), Token::Dot) {
7888            self.advance();
7889            let col = self.expect_ident_like()?;
7890            if head.eq_ignore_ascii_case("new") {
7891                return Ok(AssignTarget::NewColumn(col));
7892            }
7893            if head.eq_ignore_ascii_case("old") {
7894                return Ok(AssignTarget::OldColumn(col));
7895            }
7896            return Err(self.err(alloc::format!(
7897                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7898                 got {head:?}.<col>"
7899            )));
7900        }
7901        Ok(AssignTarget::Local(head))
7902    }
7903
7904    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7905        // RETURN NEW / OLD / NULL — bare-ident forms.
7906        match self.peek() {
7907            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7908                self.advance();
7909                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7910            }
7911            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7912                self.advance();
7913                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7914            }
7915            Token::Null => {
7916                self.advance();
7917                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7918            }
7919            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7920            // per PL/pgSQL convention.
7921            Token::Semicolon => {
7922                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7923            }
7924            _ => {}
7925        }
7926        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7927        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7928        // caller-visible effect (blocks don't return sets), so we
7929        // desugar it identically to PERFORM: parse the SELECT (or
7930        // EXECUTE dynamic) as embedded SQL that runs for side
7931        // effects and discards the result. RETURN NEXT <expr>
7932        // (single-row accumulator) queues with v7.40 SETOF function
7933        // infrastructure.
7934        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7935        // and keep going.
7936        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7937        {
7938            self.advance();
7939            let e = self.parse_expr(0)?;
7940            return Ok(PlPgSqlStmt::ReturnNext(e));
7941        }
7942        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7943        {
7944            self.advance();
7945            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7946            // rows go to the set, like the static form. It used to desugar to a
7947            // bare ExecuteDynamic, whose result was DISCARDED.
7948            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7949            {
7950                self.advance();
7951                let sql = self.parse_expr(0)?;
7952                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7953            }
7954            // Bare RETURN QUERY <select>. If the current token is
7955            // not already SELECT (e.g., the user wrote `RETURN QUERY
7956            // <projection> FROM ...` in a shorthand — rare but PG
7957            // accepts a bare projection here), splice one in. Same
7958            // trick as PERFORM.
7959            if !matches!(self.peek(), Token::Select) {
7960                self.tokens.insert(self.pos, Token::Select);
7961            }
7962            let select = self.parse_select_stmt()?;
7963            let Statement::Select(s) = select else {
7964                return Err(self.err(alloc::format!(
7965                    "expected SELECT body after RETURN QUERY, got {:?}",
7966                    self.peek()
7967                )));
7968            };
7969            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7970            // to an embedded side-effect SELECT whose rows were DISCARDED, which
7971            // in a SETOF function is the entire answer thrown away.
7972            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7973        }
7974        // Fall through: parse a full expression.
7975        let e = self.parse_expr(0)?;
7976        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7977    }
7978
7979    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7980        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7981        // are ident-shaped (the parser keys off case-insensitive
7982        // match — same shape used by the top-level Update / Delete
7983        // dispatchers at parse_one_statement).
7984        if matches!(self.peek(), Token::Insert) {
7985            self.advance();
7986            return Ok(TriggerEvent::Insert);
7987        }
7988        match self.peek() {
7989            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7990                self.advance();
7991                Ok(TriggerEvent::Update)
7992            }
7993            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7994                self.advance();
7995                Ok(TriggerEvent::Delete)
7996            }
7997            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7998                self.advance();
7999                Ok(TriggerEvent::Truncate)
8000            }
8001            other => Err(self.err(alloc::format!(
8002                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8003            ))),
8004        }
8005    }
8006
8007    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8008    ///   - (no clause) → implicit `FOR ALL TABLES`
8009    ///   - `FOR ALL TABLES`
8010    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8011    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8012    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
8013    ///     REJECTS the bare plural (`invalid publication object list`,
8014    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
8015    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8016    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8017        let name = self.expect_ident_or_string()?;
8018        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8019        // shape so existing publications keep parsing identically.
8020        let scope = if matches!(self.peek(), Token::For) {
8021            self.advance();
8022            if matches!(self.peek(), Token::All) {
8023                self.advance();
8024                if !matches!(self.peek(), Token::Tables) {
8025                    return Err(self.err(format!(
8026                        "expected TABLES after FOR ALL, got {:?}",
8027                        self.peek()
8028                    )));
8029                }
8030                self.advance();
8031                if matches!(self.peek(), Token::Except) {
8032                    self.advance();
8033                    let tables = self.parse_publication_table_list()?;
8034                    PublicationScope::AllTablesExcept(tables)
8035                } else {
8036                    PublicationScope::AllTables
8037                }
8038            } else if matches!(self.peek(), Token::Table) {
8039                self.advance();
8040                let tables = self.parse_publication_table_list()?;
8041                PublicationScope::ForTables(tables)
8042            } else if matches!(self.peek(), Token::Tables) {
8043                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8044                // plural (`FOR TABLES t`) is REJECTED (`invalid
8045                // publication object list`); TABLES only pairs with
8046                // `IN SCHEMA`. The old arm accepted it on an
8047                // unverifiable "PG 19 accepts both" claim.
8048                self.advance();
8049                if !matches!(self.peek(), Token::In) {
8050                    return Err(self.err(alloc::string::String::from(
8051                        "invalid publication object list",
8052                    )));
8053                }
8054                self.advance();
8055                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8056                    return Err(self.err(format!(
8057                        "expected SCHEMA after FOR TABLES IN, got {:?}",
8058                        self.peek()
8059                    )));
8060                }
8061                self.advance();
8062                let schema = self.expect_ident_or_string()?;
8063                PublicationScope::TablesInSchema(schema)
8064            } else {
8065                return Err(self.err(format!(
8066                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8067                    self.peek()
8068                )));
8069            }
8070        } else {
8071            PublicationScope::AllTables
8072        };
8073        Ok(Statement::CreatePublication(CreatePublicationStatement {
8074            name,
8075            scope,
8076        }))
8077    }
8078
8079    /// v6.1.3 — Comma-separated identifier list for the publication
8080    /// FOR-clause. Requires at least one entry; empty list is a
8081    /// parse error (PG behaviour). Quoted idents are accepted; the
8082    /// names round-trip through `Display` as `quote_ident(name)`.
8083    ///
8084    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8085    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8086    /// pg_dump output. SPG's publication state today is per-table
8087    /// only (matching the pre-PG-15 surface); the col list + WHERE
8088    /// are parsed so dumps load through and the table name reaches
8089    /// `PublicationScope::ForTables`, but the filter is not enforced
8090    /// at publish time. Re-open when a customer dogfood gate
8091    /// requires per-row-filter or column-subset publish semantics
8092    /// (which gates on persistent slot state landing first, 21.12).
8093    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8094        let first = self.parse_publication_table_entry()?;
8095        let mut out = alloc::vec![first];
8096        while matches!(self.peek(), Token::Comma) {
8097            self.advance();
8098            out.push(self.parse_publication_table_entry()?);
8099        }
8100        Ok(out)
8101    }
8102
8103    /// One table entry inside a FOR TABLE clause:
8104    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8105    /// Returns just the table name; the column list + WHERE predicate
8106    /// are consumed and discarded per the parse-accept-discard
8107    /// commitment above.
8108    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8109        let name = self.expect_ident_like()?;
8110        // Optional column list — `(col, col, …)`.
8111        if matches!(self.peek(), Token::LParen) {
8112            self.advance();
8113            // Empty parens are a PG error too; require ≥ 1 column.
8114            let _ = self.expect_ident_like()?;
8115            while matches!(self.peek(), Token::Comma) {
8116                self.advance();
8117                let _ = self.expect_ident_like()?;
8118            }
8119            if !matches!(self.peek(), Token::RParen) {
8120                return Err(self.err(alloc::format!(
8121                    "expected ')' to close publication column list, got {:?}",
8122                    self.peek()
8123                )));
8124            }
8125            self.advance();
8126        }
8127        // Optional row filter — `WHERE (predicate)`.
8128        if matches!(self.peek(), Token::Where) {
8129            self.advance();
8130            if !matches!(self.peek(), Token::LParen) {
8131                return Err(self.err(alloc::format!(
8132                    "expected '(' after WHERE in publication row filter, got {:?}",
8133                    self.peek()
8134                )));
8135            }
8136            self.advance();
8137            let _ = self.parse_expr(0)?;
8138            if !matches!(self.peek(), Token::RParen) {
8139                return Err(self.err(alloc::format!(
8140                    "expected ')' to close publication WHERE filter, got {:?}",
8141                    self.peek()
8142                )));
8143            }
8144            self.advance();
8145        }
8146        Ok(name)
8147    }
8148
8149    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8150    ///                 CONNECTION '<conn>'
8151    ///                 PUBLICATION <pub> [, <pub> ...]`.
8152    ///
8153    /// The clause order is fixed (CONNECTION first, then
8154    /// PUBLICATION) to match PG. No WITH-options accepted in
8155    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8156    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8157        let name = self.expect_ident_or_string()?;
8158        if !matches!(self.peek(), Token::Connection) {
8159            return Err(self.err(format!(
8160                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8161                self.peek()
8162            )));
8163        }
8164        self.advance();
8165        let conn_str = self.expect_string_literal()?;
8166        if !matches!(self.peek(), Token::Publication) {
8167            return Err(self.err(format!(
8168                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8169                self.peek()
8170            )));
8171        }
8172        self.advance();
8173        // Reuse the publication FOR-list parser shape: at least one
8174        // identifier, comma-separated.
8175        let first = self.expect_ident_like()?;
8176        let mut publications = alloc::vec![first];
8177        while matches!(self.peek(), Token::Comma) {
8178            self.advance();
8179            publications.push(self.expect_ident_like()?);
8180        }
8181        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8182            name,
8183            conn_str,
8184            publications,
8185        }))
8186    }
8187
8188    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8189    /// All keywords after `WAIT` are bare idents in v6.1.x; no
8190    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8191    /// that fit `u64`.
8192    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8193    /// qualifier is a *namespace* the app owns (`app.user_id`,
8194    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8195    /// to discard. So parse the raw segments here instead of
8196    /// `expect_ident_like`, which strips a leading `schema.` qualifier
8197    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8198    /// a single segment and round-trip unchanged.
8199    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8200        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8201        loop {
8202            let seg = match self.advance() {
8203                Token::Ident(s) | Token::QuotedIdent(s) => s,
8204                other if unreserved_keyword_text(&other).is_some() => {
8205                    unreserved_keyword_text(&other).unwrap()
8206                }
8207                other => {
8208                    return Err(ParseError {
8209                        message: format!("expected parameter name, got {other:?}"),
8210                        token_pos: self.consumed_pos(),
8211                    });
8212                }
8213            };
8214            parts.push(seg);
8215            if matches!(self.peek(), Token::Dot) {
8216                self.advance();
8217                continue;
8218            }
8219            break;
8220        }
8221        Ok(parts.join(".").to_ascii_lowercase())
8222    }
8223
8224    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8225        Self::parse_set_value_inner(self)
8226    }
8227
8228    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8229        match self.advance() {
8230            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8231            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8232                Ok(crate::ast::SetValue::Default)
8233            }
8234            Token::Ident(s) | Token::QuotedIdent(s) => {
8235                let mut accum = s;
8236                while matches!(self.peek(), Token::Dot) {
8237                    self.advance();
8238                    let next = self.expect_ident_like()?;
8239                    accum.push('.');
8240                    accum.push_str(&next);
8241                }
8242                Ok(crate::ast::SetValue::Ident(accum))
8243            }
8244            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8245            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8246            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8247            // spellings that lex as keyword tokens, not idents:
8248            // `SET standard_conforming_strings = on` is in every
8249            // pg_dump preamble (`off` already lexes as an ident).
8250            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8251            // DEFAULT lexes as its keyword token, so the ident arm above
8252            // never saw it and the everyday reset form was a syntax error.
8253            Token::Default => Ok(crate::ast::SetValue::Default),
8254            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8255            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8256            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8257            // v7.14.0 — MySQL session/user variable RHS
8258            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8259            // Wrap as Ident so the SET handler can record it; the
8260            // engine treats `@VAR` / `@@VAR` values as opaque
8261            // strings.
8262            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8263            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8264            // is the common MySQL preamble shape. Allow a `+` or
8265            // `-` prefix on negative numerics for parity with PG
8266            // (some param defaults are negative).
8267            Token::Minus => match self.advance() {
8268                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8269                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8270                other => Err(self.err(format!(
8271                    "expected numeric after `-` in SET value, got {other:?}"
8272                ))),
8273            },
8274            other => Err(self.err(format!(
8275                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8276            ))),
8277        }
8278    }
8279
8280    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8281    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8282    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8283    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8284    /// present). Modes are comma-separated per PG; SPG also
8285    /// accepts space-separated for tolerance. READ ONLY / WRITE
8286    /// / DEFERRABLE are parsed-and-ignored (recorded for future
8287    /// surface but not behaviorally honoured today).
8288    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8289    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8290    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8291    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8292    /// session default rather than forcing READ COMMITTED.
8293    fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8294        let mut level = IsolationLevel::default();
8295        let mut have_level = false;
8296        loop {
8297            // ISOLATION LEVEL …
8298            let saw_isolation =
8299                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8300            if saw_isolation {
8301                self.advance(); // ISOLATION
8302                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8303                    return Err(self.err(alloc::format!(
8304                        "expected LEVEL after ISOLATION, got {:?}",
8305                        self.peek()
8306                    )));
8307                }
8308                self.advance(); // LEVEL
8309                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8310                let w1 = self
8311                    .expect_ident_like()
8312                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8313                let lc = w1.to_ascii_lowercase();
8314                level = match lc.as_str() {
8315                    "serializable" => IsolationLevel::Serializable,
8316                    "repeatable" => {
8317                        // Expect READ
8318                        let w2 = self
8319                            .expect_ident_like()
8320                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8321                        if !w2.eq_ignore_ascii_case("read") {
8322                            return Err(self.err(alloc::format!(
8323                                "expected READ after REPEATABLE, got {w2:?}"
8324                            )));
8325                        }
8326                        IsolationLevel::RepeatableRead
8327                    }
8328                    "read" => {
8329                        let w2 = self
8330                            .expect_ident_like()
8331                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8332                        match w2.to_ascii_lowercase().as_str() {
8333                            "committed" => IsolationLevel::ReadCommitted,
8334                            "uncommitted" => IsolationLevel::ReadUncommitted,
8335                            other => {
8336                                return Err(self.err(alloc::format!(
8337                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8338                                )));
8339                            }
8340                        }
8341                    }
8342                    other => {
8343                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8344                    }
8345                };
8346                have_level = true;
8347            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8348                // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8349                self.advance();
8350                match self.peek().clone() {
8351                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8352                        self.advance();
8353                    }
8354                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8355                        self.advance();
8356                    }
8357                    other => {
8358                        return Err(self.err(alloc::format!(
8359                            "expected ONLY or WRITE after READ, got {other:?}"
8360                        )));
8361                    }
8362                }
8363            } else if matches!(self.peek(), Token::Not) {
8364                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8365                self.advance();
8366                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8367                    return Err(self.err(alloc::format!(
8368                        "expected DEFERRABLE after NOT, got {:?}",
8369                        self.peek()
8370                    )));
8371                }
8372                self.advance();
8373            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8374            {
8375                self.advance();
8376            } else {
8377                break;
8378            }
8379            // Optional comma between modes.
8380            if matches!(self.peek(), Token::Comma) {
8381                self.advance();
8382            }
8383        }
8384        Ok(have_level.then_some(level))
8385    }
8386
8387    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8388        // FOR is a v6.1.2-reserved keyword (Token::For). The
8389        // other two are bare idents — they've never needed lexer
8390        // support and we keep it that way.
8391        if !matches!(self.peek(), Token::For) {
8392            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8393        }
8394        self.advance();
8395        self.expect_keyword_ident("wal")?;
8396        self.expect_keyword_ident("position")?;
8397        let pos = self.expect_u64_literal()?;
8398        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8399        {
8400            self.advance();
8401            self.expect_keyword_ident("timeout")?;
8402            Some(self.expect_u64_literal()?)
8403        } else {
8404            None
8405        };
8406        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8407    }
8408
8409    /// v6.1.7 helper — consume a `Token::Integer` and check it
8410    /// fits `u64`. WAL positions and millisecond timeouts are
8411    /// non-negative.
8412    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8413        match self.advance() {
8414            Token::Integer(n) if n >= 0 => Ok(n as u64),
8415            Token::Integer(n) => Err(ParseError {
8416                message: format!("expected non-negative integer, got {n}"),
8417                token_pos: self.consumed_pos(),
8418            }),
8419            other => Err(ParseError {
8420                message: format!("expected integer literal, got {other:?}"),
8421                token_pos: self.consumed_pos(),
8422            }),
8423        }
8424    }
8425
8426    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8427    /// ROLE '<role>' (defaults to readonly). All string slots accept
8428    /// either a quoted ident or a quoted string literal.
8429    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8430    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8431    ///
8432    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8433    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8434    /// wire role) still parses — it is a different axis from the PG attributes.
8435    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8436    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8437    /// or RESET, so the plain attribute forms keep their old path.
8438    fn peeks_db_role_setting(&self) -> bool {
8439        let mut i = self.pos + 1; // past the object's name
8440        let word = |p: usize| -> Option<String> {
8441            match self.tokens.get(p) {
8442                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8443                Some(Token::In) => Some(String::from("in")),
8444                _ => None,
8445            }
8446        };
8447        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8448            i += 3; // IN DATABASE <name>
8449        }
8450        matches!(word(i).as_deref(), Some("set" | "reset"))
8451    }
8452
8453    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8454        use crate::ast::SetDbRoleSettingStatement;
8455        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8456        // identifier, so the ordinary name reader refuses it. Same trap
8457        // as TABLE / INDEX / FULL / DEFAULT before it.
8458        let name = if matches!(self.peek(), Token::All) {
8459            self.advance();
8460            String::from("all")
8461        } else {
8462            self.expect_ident_or_string()?
8463        };
8464        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8465        let all = name.eq_ignore_ascii_case("all");
8466        let (mut database, mut role) = if is_database {
8467            (Some(name), None)
8468        } else if all {
8469            (None, None)
8470        } else {
8471            (None, Some(name))
8472        };
8473        if matches!(self.peek(), Token::In) {
8474            self.advance();
8475            self.advance(); // DATABASE
8476            database = Some(self.expect_ident_or_string()?);
8477        }
8478        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8479        self.advance(); // SET | RESET
8480        if resetting && matches!(self.peek(), Token::All) {
8481            self.advance();
8482            self.consume_until_statement_boundary();
8483            return Ok(Statement::SetDbRoleSetting(Box::new(
8484                SetDbRoleSettingStatement {
8485                    database,
8486                    role,
8487                    param: None,
8488                    value: None,
8489                },
8490            )));
8491        }
8492        let param = self.expect_ident_like()?;
8493        let value = if resetting {
8494            None
8495        } else {
8496            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8497            // KEYWORD, so the ident-only check missed it and consumed
8498            // the word itself as the value — the same trap as ALL, one
8499            // clause over.
8500            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8501                self.advance();
8502            }
8503            Some(self.take_guc_value())
8504        };
8505        self.consume_until_statement_boundary();
8506        Ok(Statement::SetDbRoleSetting(Box::new(
8507            SetDbRoleSettingStatement {
8508                database,
8509                role,
8510                param: Some(param),
8511                value,
8512            },
8513        )))
8514    }
8515
8516    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8517    /// a quoted literal loses its quotes, a bare word or number does not.
8518    fn take_guc_value(&mut self) -> String {
8519        match self.advance() {
8520            Token::String(s) => s,
8521            Token::Integer(n) => format!("{n}"),
8522            Token::Float(f) => format!("{f}"),
8523            Token::Ident(s) | Token::QuotedIdent(s) => s,
8524            other => format!("{other:?}"),
8525        }
8526    }
8527
8528    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8529        let name = self.expect_ident_or_string()?;
8530        if self.peek_keyword_ident("with") {
8531            self.advance();
8532        }
8533        let mut password = String::new();
8534        let mut role = String::new();
8535        let mut login: Option<bool> = None;
8536        let mut inherit: Option<bool> = None;
8537        let mut superuser: Option<bool> = None;
8538        // Not a `while let`: the pattern would borrow `self` across the
8539        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8540        #[allow(clippy::while_let_loop)]
8541        loop {
8542            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8543                break;
8544            };
8545            match w.to_ascii_lowercase().as_str() {
8546                "password" => {
8547                    self.advance();
8548                    password = self.expect_string_literal()?;
8549                }
8550                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8551                // is the same slot.
8552                "encrypted" => {
8553                    self.advance();
8554                    self.expect_keyword_ident("password")?;
8555                    password = self.expect_string_literal()?;
8556                }
8557                "login" => {
8558                    self.advance();
8559                    login = Some(true);
8560                }
8561                "nologin" => {
8562                    self.advance();
8563                    login = Some(false);
8564                }
8565                "inherit" => {
8566                    self.advance();
8567                    inherit = Some(true);
8568                }
8569                "noinherit" => {
8570                    self.advance();
8571                    inherit = Some(false);
8572                }
8573                "superuser" => {
8574                    self.advance();
8575                    superuser = Some(true);
8576                }
8577                "nosuperuser" => {
8578                    self.advance();
8579                    superuser = Some(false);
8580                }
8581                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8582                "role" => {
8583                    self.advance();
8584                    role = self.expect_string_literal()?;
8585                }
8586                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8587                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8588                // accepted and ignored so a pg_dump role block restores. They
8589                // gate capabilities SPG does not have.
8590                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8591                | "noreplication" | "bypassrls" | "nobypassrls" => {
8592                    self.advance();
8593                }
8594                "connection" => {
8595                    self.advance();
8596                    self.expect_keyword_ident("limit")?;
8597                    self.advance(); // the number
8598                }
8599                "valid" => {
8600                    self.advance();
8601                    self.expect_keyword_ident("until")?;
8602                    self.expect_string_literal()?;
8603                }
8604                _ => break,
8605            }
8606        }
8607        if role.is_empty() {
8608            role = "readonly".to_string();
8609        }
8610        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8611            name,
8612            password,
8613            role,
8614            login,
8615            inherit,
8616            superuser,
8617            is_user,
8618        }))
8619    }
8620
8621    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8622    /// consumed the USING / WITH CHECK keyword.
8623    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8624        if !matches!(self.peek(), Token::LParen) {
8625            return Err(self.err(alloc::format!(
8626                "expected '(' after {clause}, got {:?}",
8627                self.peek()
8628            )));
8629        }
8630        self.advance();
8631        let e = self.parse_expr(0)?;
8632        if !matches!(self.peek(), Token::RParen) {
8633            return Err(self.err(alloc::format!(
8634                "expected ')' to close {clause}, got {:?}",
8635                self.peek()
8636            )));
8637        }
8638        self.advance();
8639        Ok(e)
8640    }
8641
8642    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8643    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8644        let mut roles = Vec::new();
8645        loop {
8646            roles.push(self.expect_ident_like()?);
8647            if matches!(self.peek(), Token::Comma) {
8648                self.advance();
8649            } else {
8650                break;
8651            }
8652        }
8653        Ok(roles)
8654    }
8655
8656    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8657    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8658    /// `CREATE POLICY`.
8659    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8660        use crate::ast::PolicyCmd;
8661        let name = self.expect_ident_like()?;
8662        if !matches!(self.peek(), Token::On) {
8663            return Err(self.err(alloc::format!(
8664                "expected ON after CREATE POLICY name, got {:?}",
8665                self.peek()
8666            )));
8667        }
8668        self.advance();
8669        let table = self.expect_ident_like()?;
8670
8671        let mut permissive = true;
8672        if matches!(self.peek(), Token::As) {
8673            self.advance();
8674            let w = self.expect_ident_like()?;
8675            permissive = if w.eq_ignore_ascii_case("permissive") {
8676                true
8677            } else if w.eq_ignore_ascii_case("restrictive") {
8678                false
8679            } else {
8680                return Err(self.err(alloc::format!(
8681                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8682                )));
8683            };
8684        }
8685
8686        let mut cmd = PolicyCmd::All;
8687        if matches!(self.peek(), Token::For) {
8688            self.advance();
8689            cmd = self.parse_policy_cmd()?;
8690        }
8691
8692        let mut roles = Vec::new();
8693        if matches!(self.peek(), Token::To) {
8694            self.advance();
8695            roles = self.parse_policy_roles()?;
8696        }
8697
8698        let mut using = None;
8699        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8700        {
8701            self.advance();
8702            using = Some(self.parse_paren_expr("USING")?);
8703        }
8704
8705        let mut with_check = None;
8706        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8707        {
8708            self.advance();
8709            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8710            {
8711                return Err(self.err(alloc::format!(
8712                    "expected CHECK after WITH, got {:?}",
8713                    self.peek()
8714                )));
8715            }
8716            self.advance();
8717            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8718        }
8719
8720        // Clause-per-command matrix (PG wording).
8721        match cmd {
8722            PolicyCmd::Insert => {
8723                if using.is_some() {
8724                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8725                }
8726            }
8727            PolicyCmd::Select | PolicyCmd::Delete => {
8728                if with_check.is_some() {
8729                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8730                }
8731            }
8732            PolicyCmd::Update | PolicyCmd::All => {}
8733        }
8734
8735        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8736            name,
8737            table,
8738            permissive,
8739            cmd,
8740            roles,
8741            using,
8742            with_check,
8743        }))
8744    }
8745
8746    /// v7.39 (RLS) — the command word after `FOR`.
8747    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8748        use crate::ast::PolicyCmd;
8749        match self.peek().clone() {
8750            Token::All => {
8751                self.advance();
8752                Ok(PolicyCmd::All)
8753            }
8754            Token::Select => {
8755                self.advance();
8756                Ok(PolicyCmd::Select)
8757            }
8758            Token::Insert => {
8759                self.advance();
8760                Ok(PolicyCmd::Insert)
8761            }
8762            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8763                self.advance();
8764                Ok(PolicyCmd::Update)
8765            }
8766            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8767                self.advance();
8768                Ok(PolicyCmd::Delete)
8769            }
8770            other => Err(self.err(alloc::format!(
8771                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8772            ))),
8773        }
8774    }
8775
8776    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8777    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8778    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8779        let name = self.expect_ident_like()?;
8780        if !matches!(self.peek(), Token::On) {
8781            return Err(self.err(alloc::format!(
8782                "expected ON after ALTER POLICY name, got {:?}",
8783                self.peek()
8784            )));
8785        }
8786        self.advance();
8787        let table = self.expect_ident_like()?;
8788
8789        // RENAME TO new
8790        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8791        {
8792            self.advance();
8793            if !matches!(self.peek(), Token::To) {
8794                return Err(self.err(alloc::format!(
8795                    "expected TO after RENAME, got {:?}",
8796                    self.peek()
8797                )));
8798            }
8799            self.advance();
8800            let new = self.expect_ident_like()?;
8801            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8802                name,
8803                table,
8804                rename_to: Some(new),
8805                roles: None,
8806                using: None,
8807                with_check: None,
8808            }));
8809        }
8810
8811        let mut roles = None;
8812        if matches!(self.peek(), Token::To) {
8813            self.advance();
8814            roles = Some(self.parse_policy_roles()?);
8815        }
8816        let mut using = None;
8817        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8818        {
8819            self.advance();
8820            using = Some(self.parse_paren_expr("USING")?);
8821        }
8822        let mut with_check = None;
8823        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8824        {
8825            self.advance();
8826            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8827            {
8828                return Err(self.err(alloc::format!(
8829                    "expected CHECK after WITH, got {:?}",
8830                    self.peek()
8831                )));
8832            }
8833            self.advance();
8834            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8835        }
8836        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8837            name,
8838            table,
8839            rename_to: None,
8840            roles,
8841            using,
8842            with_check,
8843        }))
8844    }
8845
8846    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8847    /// `DROP POLICY`.
8848    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8849        let if_exists = self.consume_if_exists();
8850        let name = self.expect_ident_like()?;
8851        if !matches!(self.peek(), Token::On) {
8852            return Err(self.err(alloc::format!(
8853                "expected ON after DROP POLICY name, got {:?}",
8854                self.peek()
8855            )));
8856        }
8857        self.advance();
8858        let table = self.expect_ident_like()?;
8859        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8860            name,
8861            table,
8862            if_exists,
8863        }))
8864    }
8865}
8866fn wrap_from_leaves(
8867    e: &mut Expr,
8868    names: &[String],
8869    make: &dyn Fn(Expr) -> Expr,
8870    refs: &dyn Fn(&Expr) -> bool,
8871) {
8872    if let Expr::Column(c) = e {
8873        if c.qualifier
8874            .as_deref()
8875            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8876        {
8877            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8878            *e = make(taken);
8879        }
8880        return;
8881    }
8882    match e {
8883        Expr::Binary { lhs, rhs, .. } => {
8884            wrap_from_leaves(lhs, names, make, refs);
8885            wrap_from_leaves(rhs, names, make, refs);
8886        }
8887        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8888            wrap_from_leaves(expr, names, make, refs)
8889        }
8890        Expr::FunctionCall { args, .. } => {
8891            for a in args.iter_mut() {
8892                wrap_from_leaves(a, names, make, refs);
8893            }
8894        }
8895        Expr::Case {
8896            operand,
8897            branches,
8898            else_branch,
8899        } => {
8900            if let Some(o) = operand.as_deref_mut() {
8901                wrap_from_leaves(o, names, make, refs);
8902            }
8903            for (w, t) in branches.iter_mut() {
8904                wrap_from_leaves(w, names, make, refs);
8905                wrap_from_leaves(t, names, make, refs);
8906            }
8907            if let Some(el) = else_branch.as_deref_mut() {
8908                wrap_from_leaves(el, names, make, refs);
8909            }
8910        }
8911        // Compound variants the walk doesn't decompose: keep the
8912        // pre-D.30 behavior — wrap the whole sub-expr if it touches
8913        // a source table, so nothing regresses.
8914        other => {
8915            if refs(other) {
8916                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8917                *other = make(taken);
8918            }
8919        }
8920    }
8921}
8922
8923/// v7.39 (round 241) — does this expression reference any of the FROM /
8924/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8925/// lowerings)?
8926fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8927    match e {
8928        Expr::Column(c) => c
8929            .qualifier
8930            .as_deref()
8931            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8932        Expr::Binary { lhs, rhs, .. } => {
8933            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8934        }
8935        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8936        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8937        Expr::Case {
8938            operand,
8939            branches,
8940            else_branch,
8941        } => {
8942            operand
8943                .as_deref()
8944                .is_some_and(|o| expr_refs_tables(o, names))
8945                || branches
8946                    .iter()
8947                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8948                || else_branch
8949                    .as_deref()
8950                    .is_some_and(|el| expr_refs_tables(el, names))
8951        }
8952        _ => false,
8953    }
8954}
8955
8956impl Parser {
8957    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8958    /// Caller already consumed the leading `UPDATE` ident.
8959    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8960    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8961    /// after the target name has been read. `JOIN` is a reserved token;
8962    /// the qualifiers are bare idents.
8963    fn peek_is_update_join_start(&self) -> bool {
8964        match self.peek() {
8965            // JOIN and its qualifiers are reserved lexer tokens (the grammar
8966            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8967            Token::Join
8968            | Token::Inner
8969            | Token::Left
8970            | Token::Right
8971            | Token::Cross
8972            | Token::Full => true,
8973            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8974            Token::Ident(s) | Token::QuotedIdent(s) => {
8975                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8976            }
8977            _ => false,
8978        }
8979    }
8980
8981    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8982    /// USER-variable assignment. Its own per-session namespace, an arbitrary
8983    /// expression on the right, and `:=` as a second spelling of `=`.
8984    ///
8985    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8986    /// from sits on the nesting recursion chain (a CTE body, a subquery),
8987    /// and holding this loop's `Vec` + `String` locals there overflowed the
8988    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8989    #[inline(never)]
8990    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8991        let mut assigns: Vec<(String, Expr)> = Vec::new();
8992        let mut settings: Vec<(String, Expr)> = Vec::new();
8993        loop {
8994            // v7.39 (round 554) — a plain NAME here is a session
8995            // setting, not a user variable. mysqldump writes the two in
8996            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8997            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8998            // changes it — and this refused the mixture outright, so no
8999            // dump could be restored past its preamble.
9000            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9001                self.advance();
9002                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9003                    return Err(self.err(alloc::format!(
9004                        "expected `=` after {name}, got {:?}",
9005                        self.peek()
9006                    )));
9007                }
9008                self.advance();
9009                let value = self.parse_expr(0)?;
9010                settings.push((name.to_ascii_lowercase(), value));
9011                if matches!(self.peek(), Token::Comma) {
9012                    self.advance();
9013                    continue;
9014                }
9015                break;
9016            }
9017            let Token::SessionVar(raw) = self.peek().clone() else {
9018                return Err(self.err(alloc::format!(
9019                    "expected a user variable after SET, got {:?}",
9020                    self.peek()
9021                )));
9022            };
9023            if raw.starts_with("@@") {
9024                return Err(self.err(alloc::string::String::from(
9025                    "cannot mix `@@` settings with `@` user variables in one SET",
9026                )));
9027            }
9028            self.advance();
9029            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9030                return Err(self.err(alloc::format!(
9031                    "expected `=` or `:=` after {raw}, got {:?}",
9032                    self.peek()
9033                )));
9034            }
9035            self.advance();
9036            let value = self.parse_expr(0)?;
9037            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9038            if matches!(self.peek(), Token::Comma) {
9039                self.advance();
9040                continue;
9041            }
9042            break;
9043        }
9044        Ok(Statement::SetUserVars(assigns, settings))
9045    }
9046
9047    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9048        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9049        // NAMED `only` until now, which failed on `relation "only" does
9050        // not exist`. The lookahead is what keeps a table actually
9051        // called `only` working: the keyword is only a keyword when a
9052        // TABLE NAME follows it — and `SET` arrives as an identifier
9053        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9054        // for the table and die on the `=`. Measured by the pin.
9055        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9056            if s.eq_ignore_ascii_case("only"))
9057            && matches!(
9058                self.tokens.get(self.pos + 1),
9059                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9060            );
9061        if only {
9062            self.advance();
9063        }
9064        let table = self.expect_ident_like()?;
9065        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9066        // bare spelling; a bare identifier that is the SET keyword itself
9067        // is the clause, not an alias.
9068        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9069        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9070        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9071        // following JOIN a syntax error.
9072        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9073        let alias = if matches!(self.peek(), Token::As) {
9074            self.advance();
9075            Some(self.expect_ident_like()?)
9076        } else {
9077            match self.peek() {
9078                Token::Ident(s) | Token::QuotedIdent(s)
9079                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
9080                {
9081                    let a = s.clone();
9082                    self.advance();
9083                    Some(a)
9084                }
9085                _ => None,
9086            }
9087        };
9088        // v7.39 (round 420) — MySQL's multi-table UPDATE:
9089        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
9090        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
9091        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9092        // The FIRST table is the mutation target and the rest are sources —
9093        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9094        // SPG already lowers onto correlated subqueries. So rewind, let
9095        // `parse_from_clause` read the whole list (it handles aliases, comma
9096        // lists, and every JOIN form), then peel the target off the front.
9097        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9098            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9099        {
9100            // NOTE: `advance()` destroys the tokens it returns
9101            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9102            // is NOT possible — the tail is read forward, once, through the
9103            // same grammar `parse_from_clause` uses after its primary.
9104            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9105            let mut joins = self.parse_from_joins(&target_qual)?;
9106            if joins.is_empty() {
9107                return Err(self.err(alloc::string::String::from(
9108                    "multi-table UPDATE needs at least one source table",
9109                )));
9110            }
9111            let head = joins.remove(0);
9112            // A LEFT join keeps every target row (the unmatched ones see NULL
9113            // on the source side), so it must NOT get the EXISTS row filter
9114            // the inner / comma forms use.
9115            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9116            let src = FromClause {
9117                primary: head.table,
9118                joins,
9119            };
9120            (Some(src), head.on, outer)
9121        } else {
9122            (None, None, false)
9123        };
9124        self.expect_keyword_ident("set")?;
9125        let mut assignments = Vec::new();
9126        loop {
9127            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9128            // …)` — the parenthesized multi-assignment. Expressions
9129            // assign positionally; a subquery RHS clones per column
9130            // keeping only the Nth projection item.
9131            if matches!(self.peek(), Token::LParen) {
9132                self.advance();
9133                let mut cols = alloc::vec![self.expect_ident_like()?];
9134                while matches!(self.peek(), Token::Comma) {
9135                    self.advance();
9136                    cols.push(self.expect_ident_like()?);
9137                }
9138                if !matches!(self.peek(), Token::RParen) {
9139                    return Err(self.err(format!(
9140                        "expected ')' after SET column list, got {:?}",
9141                        self.peek()
9142                    )));
9143                }
9144                self.advance();
9145                if !matches!(self.peek(), Token::Eq) {
9146                    return Err(self.err(format!(
9147                        "expected `=` after SET column list, got {:?}",
9148                        self.peek()
9149                    )));
9150                }
9151                self.advance();
9152                if !matches!(self.peek(), Token::LParen) {
9153                    return Err(self.err(format!(
9154                        "expected '(' after SET (…) =, got {:?}",
9155                        self.peek()
9156                    )));
9157                }
9158                self.advance();
9159                if matches!(self.peek(), Token::Select) {
9160                    let inner = match self.parse_select_stmt()? {
9161                        Statement::Select(s) => s,
9162                        other => {
9163                            return Err(self.err(alloc::format!(
9164                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9165                            )));
9166                        }
9167                    };
9168                    if !matches!(self.peek(), Token::RParen) {
9169                        return Err(self.err(format!(
9170                            "expected ')' after SET subquery, got {:?}",
9171                            self.peek()
9172                        )));
9173                    }
9174                    self.advance();
9175                    if inner.items.len() != cols.len() {
9176                        return Err(self.err(alloc::format!(
9177                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9178                            cols.len(),
9179                            inner.items.len()
9180                        )));
9181                    }
9182                    for (i, col) in cols.into_iter().enumerate() {
9183                        let mut sub = inner.clone();
9184                        sub.items = alloc::vec![sub.items[i].clone()];
9185                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9186                    }
9187                } else {
9188                    let mut exprs = alloc::vec![self.parse_expr(0)?];
9189                    while matches!(self.peek(), Token::Comma) {
9190                        self.advance();
9191                        exprs.push(self.parse_expr(0)?);
9192                    }
9193                    if !matches!(self.peek(), Token::RParen) {
9194                        return Err(self.err(format!(
9195                            "expected ')' after SET row values, got {:?}",
9196                            self.peek()
9197                        )));
9198                    }
9199                    self.advance();
9200                    if exprs.len() != cols.len() {
9201                        return Err(self.err(alloc::format!(
9202                            "SET (…) = (…) arity mismatch: {} columns, {} values",
9203                            cols.len(),
9204                            exprs.len()
9205                        )));
9206                    }
9207                    for (col, e) in cols.into_iter().zip(exprs) {
9208                        assignments.push((col, e));
9209                    }
9210                }
9211                if matches!(self.peek(), Token::Comma) {
9212                    self.advance();
9213                    continue;
9214                }
9215                break;
9216            }
9217            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9218            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9219            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9220            // `public.` dump qualifiers), so the qualifier has to be read off
9221            // the token stream first — otherwise `SET b.v = 888` would write
9222            // to the TARGET table's `v` while naming a source table, a
9223            // silent-wrong. A qualifier naming a SOURCE table means a
9224            // multi-TARGET update — mutating two tables in one statement —
9225            // which SPG does not model, so it is refused loudly.
9226            let set_qual: Option<String> = if mysql_from.is_some()
9227                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9228            {
9229                match self.peek() {
9230                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9231                    _ => None,
9232                }
9233            } else {
9234                None
9235            };
9236            let col = self.expect_ident_like()?;
9237            if let Some(q) = set_qual {
9238                let names_target = q.eq_ignore_ascii_case(&table)
9239                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9240                if !names_target {
9241                    return Err(self.err(alloc::format!(
9242                        "multi-table UPDATE can only assign to its first table \
9243                         ({table}); `{q}.{col}` targets another table"
9244                    )));
9245                }
9246            }
9247            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9248            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9249            // `__column_default` marker lowering just below). PG assigns to the
9250            // i-th (1-based) element, NULL-padding when i exceeds the length.
9251            if matches!(self.peek(), Token::LBracket) {
9252                self.advance();
9253                let index = self.parse_expr(0)?;
9254                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9255                // (and the open `arr[lo:]`), lowered to
9256                // `__array_assign_slice`. Only the single-subscript form
9257                // parsed before, so a slice assignment was a syntax error.
9258                let mut slice_hi: Option<Option<Expr>> = None;
9259                if matches!(self.peek(), Token::Colon) {
9260                    self.advance();
9261                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9262                        None
9263                    } else {
9264                        Some(self.parse_expr(0)?)
9265                    });
9266                }
9267                if !matches!(self.peek(), Token::RBracket) {
9268                    return Err(self.err(format!(
9269                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9270                        self.peek()
9271                    )));
9272                }
9273                self.advance();
9274                if !matches!(self.peek(), Token::Eq) {
9275                    return Err(self.err(format!(
9276                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9277                        self.peek()
9278                    )));
9279                }
9280                self.advance();
9281                let value = self.parse_expr(0)?;
9282                // PG merges several subscript writes to the same column into one
9283                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9284                // assignment to `col` rather than each overwriting the original.
9285                let existing = assignments.iter().position(|(c, _)| c == &col);
9286                let base = match existing {
9287                    Some(i) => assignments[i].1.clone(),
9288                    None => Expr::Column(ColumnName {
9289                        qualifier: None,
9290                        name: col.clone(),
9291                    }),
9292                };
9293                let assigned = match slice_hi {
9294                    None => Expr::FunctionCall {
9295                        name: "__array_assign".to_string(),
9296                        args: alloc::vec![base, index, value],
9297                    },
9298                    Some(hi) => Expr::FunctionCall {
9299                        name: "__array_assign_slice".to_string(),
9300                        args: alloc::vec![
9301                            base,
9302                            index,
9303                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9304                            value,
9305                        ],
9306                    },
9307                };
9308                match existing {
9309                    Some(i) => assignments[i].1 = assigned,
9310                    None => assignments.push((col, assigned)),
9311                }
9312                if matches!(self.peek(), Token::Comma) {
9313                    self.advance();
9314                    continue;
9315                }
9316                break;
9317            }
9318            if !matches!(self.peek(), Token::Eq) {
9319                return Err(self.err(format!(
9320                    "expected `=` after column name in UPDATE SET, got {:?}",
9321                    self.peek()
9322                )));
9323            }
9324            self.advance();
9325            // `SET col = DEFAULT` — the column's declared default;
9326            // rides out as a marker call the update executor
9327            // resolves against the schema.
9328            let value = if matches!(self.peek(), Token::Default) {
9329                self.advance();
9330                Expr::FunctionCall {
9331                    name: "__column_default".to_string(),
9332                    args: Vec::new(),
9333                }
9334            } else {
9335                self.parse_expr(0)?
9336            };
9337            assignments.push((col, value));
9338            if matches!(self.peek(), Token::Comma) {
9339                self.advance();
9340                continue;
9341            }
9342            break;
9343        }
9344        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9345        // update. Lowers onto the correlated-subquery machinery:
9346        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9347        // and each assignment that references a FROM-list table
9348        // wraps into a correlated scalar subquery
9349        // (SELECT expr FROM src WHERE cond). Equivalent for the
9350        // unique-join shape (the overwhelmingly common one); a
9351        // multi-match, which PG resolves by arbitrary pick,
9352        // surfaces as a scalar-subquery cardinality error instead
9353        // of a silent arbitrary result.
9354        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9355        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9356        // the SAME lowering below. Both spellings together is not legal in
9357        // either dialect.
9358        let from_clause = if let Some(fc) = mysql_from {
9359            if matches!(self.peek(), Token::From) {
9360                return Err(self.err(alloc::string::String::from(
9361                    "multi-table UPDATE already names its sources; drop the FROM clause",
9362                )));
9363            }
9364            Some(fc)
9365        } else if matches!(self.peek(), Token::From) {
9366            self.advance();
9367            Some(self.parse_from_clause()?)
9368        } else {
9369            None
9370        };
9371        let where_ = if matches!(self.peek(), Token::Where) {
9372            self.advance();
9373            Some(self.parse_expr(0)?)
9374        } else {
9375            None
9376        };
9377        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9378        // and the TARGET-row filter are NOT the same predicate once a LEFT
9379        // join is involved:
9380        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9381        //     one conjunction, and the whole thing filters target rows via
9382        //     EXISTS.
9383        //   * LEFT join: only the ON predicate belongs inside the source
9384        //     subquery. The WHERE still filters TARGET rows (with source
9385        //     columns read through the correlated subquery, which yields NULL
9386        //     for an unmatched row — exactly LEFT-join semantics).
9387        // Round 420 folded ON into WHERE unconditionally and then dropped the
9388        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9389        // WHERE a.id > 1` updated EVERY row.
9390        let sub_where = match (mysql_on.clone(), where_.clone()) {
9391            _ if mysql_outer => mysql_on.clone(),
9392            (Some(on), Some(w)) => Some(Expr::Binary {
9393                lhs: Box::new(on),
9394                op: crate::ast::BinOp::And,
9395                rhs: Box::new(w),
9396            }),
9397            (Some(on), None) => Some(on),
9398            (None, w) => w,
9399        };
9400        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9401        // has no such clause on UPDATE, so this is accepted only under the
9402        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9403        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9404        let mut returning = self.parse_optional_returning()?;
9405        // v7.39 (round 533) — kept for the engine, which can resolve the
9406        // UNQUALIFIED leaves this lowering has to leave alone.
9407        let from_sources = from_clause.as_ref().map(|fc| {
9408            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9409                from: fc.clone(),
9410                sub_where: sub_where.clone(),
9411            })
9412        });
9413        let (assignments, where_) = if let Some(fc) = from_clause {
9414            let names: Vec<String> = core::iter::once(&fc.primary)
9415                .chain(fc.joins.iter().map(|j| &j.table))
9416                .flat_map(|t| {
9417                    t.alias
9418                        .clone()
9419                        .into_iter()
9420                        .chain(core::iter::once(t.name.clone()))
9421                })
9422                .collect();
9423            let refs_list = |e: &Expr| -> bool {
9424                fn walk(e: &Expr, names: &[String]) -> bool {
9425                    match e {
9426                        Expr::Column(c) => c
9427                            .qualifier
9428                            .as_deref()
9429                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9430                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9431                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9432                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9433                        Expr::Case {
9434                            operand,
9435                            branches,
9436                            else_branch,
9437                        } => {
9438                            operand.as_deref().is_some_and(|o| walk(o, names))
9439                                || branches
9440                                    .iter()
9441                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9442                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9443                        }
9444                        _ => false,
9445                    }
9446                }
9447                walk(e, &names)
9448            };
9449            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9450                locking: None,
9451                ctes: Vec::new(),
9452                distinct: false,
9453                distinct_on: Vec::new(),
9454                items,
9455                from: Some(fc.clone()),
9456                where_: sub_where.clone(),
9457                group_by: None,
9458                group_by_all: false,
9459                having: None,
9460                unions: Vec::new(),
9461                order_by: Vec::new(),
9462                limit: None,
9463                offset: None,
9464                limit_with_ties: false,
9465                window_check_exprs: Vec::new(),
9466            };
9467            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9468            // assignment RHS with a correlated scalar subquery, instead of
9469            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9470            // column reference (`SET v = v + u.bonus`, where `v` is the target
9471            // table's column) inside a subquery whose FROM only has the source
9472            // table, so the unqualified `v` resolved against the source and
9473            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9474            // context — where they belong — fixes it; only the source columns
9475            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9476            // compound variants the leaf-walk doesn't decompose.
9477            let make_subq = |inner: Expr| {
9478                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9479                    expr: inner,
9480                    alias: None,
9481                }])))
9482            };
9483            let assignments = assignments
9484                .into_iter()
9485                .map(|(col, mut expr)| {
9486                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9487                    (col, expr)
9488                })
9489                .collect();
9490            let exists = Expr::Exists {
9491                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9492                    expr: Expr::Literal(Literal::Integer(1)),
9493                    alias: None,
9494                }])),
9495                negated: false,
9496            };
9497            // v7.39 (round 241) — RETURNING may reference the FROM-list
9498            // tables too (`RETURNING emp.id, dept.name`); the same
9499            // leaf-to-correlated-subquery lowering the assignments get.
9500            // Without it the qualifier died at eval with "unknown table
9501            // qualifier". (RETURNING was parsed before this block — the
9502            // lowering is a pure AST transformation.)
9503            if let Some(items) = returning.as_mut() {
9504                for item in items.iter_mut() {
9505                    if let SelectItem::Expr { expr, .. } = item {
9506                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9507                    }
9508                }
9509            }
9510            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9511            // EVERY matching target row: it gets no EXISTS filter, but the
9512            // caller's WHERE still applies, with source columns read through
9513            // the correlated subquery (NULL when unmatched — LEFT-join
9514            // semantics). `sub_where` above already excluded the WHERE from
9515            // the source subquery for this case.
9516            if mysql_outer {
9517                let mut outer = where_;
9518                if let Some(w) = outer.as_mut() {
9519                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9520                }
9521                (assignments, outer)
9522            } else {
9523                (assignments, Some(exists))
9524            }
9525        } else {
9526            (assignments, where_)
9527        };
9528        Ok(Statement::Update(crate::ast::UpdateStatement {
9529            ctes: Vec::new(),
9530            table,
9531            only,
9532            alias,
9533            assignments,
9534            from_sources,
9535            where_,
9536            order_limit: update_order_limit,
9537            returning,
9538        }))
9539    }
9540
9541    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9542    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9543    /// clause and its meaning are identical, so both call this rather than
9544    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9545    /// legal. PG has no such clause on either statement, so it is read only
9546    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9547    /// errors.
9548    ///
9549    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9550    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9551    /// stack in round 430.
9552    #[inline(never)]
9553    fn parse_mysql_dml_order_limit(
9554        &mut self,
9555        what: &str,
9556    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9557        if !self.mysql_dialect {
9558            return Ok(None);
9559        }
9560        let order_by = self.parse_order_by_keys()?;
9561        let limit = if matches!(self.peek(), Token::Limit) {
9562            self.advance();
9563            let tok = self.advance();
9564            let Token::Integer(n) = tok else {
9565                return Err(self.err(alloc::format!(
9566                    "expected integer after {what} LIMIT, got {tok:?}"
9567                )));
9568            };
9569            // MySQL rejects the `LIMIT offset, count` form here — only a
9570            // single row count is legal on a DML statement.
9571            if matches!(self.peek(), Token::Comma) {
9572                return Err(self.err(alloc::format!(
9573                    "{what} LIMIT takes a row count, not an offset"
9574                )));
9575            }
9576            let n = u32::try_from(n)
9577                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9578            Some(n)
9579        } else {
9580            None
9581        };
9582        if order_by.is_empty() && limit.is_none() {
9583            return Ok(None);
9584        }
9585        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9586            order_by,
9587            limit,
9588        })))
9589    }
9590
9591    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9592    /// the leading `DELETE` ident.
9593    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9594        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9595        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9596        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9597        // parse here; it reaches the existing USING path with the target
9598        // repeated in the list, which the source-list peel below handles.)
9599        // More than one name is a multi-TARGET delete, which SPG does not
9600        // model; it is refused rather than half-applied.
9601        let mysql_pre_target: Option<String> =
9602            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9603                let first = self.expect_ident_like()?;
9604                if matches!(self.peek(), Token::Comma) {
9605                    return Err(self.err(alloc::format!(
9606                        "multi-table DELETE can only delete from one table; \
9607                     `DELETE {first}, …` names several"
9608                    )));
9609                }
9610                Some(first)
9611            } else {
9612                None
9613            };
9614        if !matches!(self.peek(), Token::From) {
9615            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9616        }
9617        self.advance();
9618        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9619        // lookahead as the UPDATE spelling.
9620        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9621            if s.eq_ignore_ascii_case("only"))
9622            && matches!(
9623                self.tokens.get(self.pos + 1),
9624                Some(Token::Ident(_) | Token::QuotedIdent(_))
9625            );
9626        if only {
9627            self.advance();
9628        }
9629        let table = self.expect_ident_like()?;
9630        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9631        // spelling must not swallow the clause keywords that can follow
9632        // the target.
9633        let alias = if matches!(self.peek(), Token::As) {
9634            self.advance();
9635            Some(self.expect_ident_like()?)
9636        } else {
9637            match self.peek() {
9638                Token::Ident(s) | Token::QuotedIdent(s)
9639                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9640                {
9641                    let a = s.clone();
9642                    self.advance();
9643                    Some(a)
9644                }
9645                _ => None,
9646            }
9647        };
9648        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9649        // through the SAME join grammar the FROM clause uses (see the
9650        // `advance()`-destroys-tokens note on `parse_from_joins`).
9651        let mut mysql_on: Option<Expr> = None;
9652        let mut mysql_outer = false;
9653        let mysql_using = if mysql_pre_target.is_some()
9654            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9655        {
9656            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9657            let mut joins = self.parse_from_joins(&target_qual)?;
9658            if joins.is_empty() {
9659                return Err(self.err(alloc::string::String::from(
9660                    "multi-table DELETE needs at least one source table",
9661                )));
9662            }
9663            let head = joins.remove(0);
9664            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9665            mysql_on = head.on;
9666            Some(FromClause {
9667                primary: head.table,
9668                joins,
9669            })
9670        } else {
9671            None
9672        };
9673        // The pre-FROM target must be the table the FROM names (or its
9674        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9675        // is not the scan target.
9676        if let Some(t) = &mysql_pre_target {
9677            let names_target = t.eq_ignore_ascii_case(&table)
9678                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9679            if !names_target {
9680                return Err(self.err(alloc::format!(
9681                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9682                )));
9683            }
9684        }
9685        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9686        // delete. Same lowering as UPDATE … FROM: the WHERE
9687        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9688        // target row by the correlated machinery.
9689        let using_clause = if let Some(fc) = mysql_using {
9690            Some(fc)
9691        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9692            self.advance();
9693            let mut fc = self.parse_from_clause()?;
9694            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9695            // repeats the TARGET as the first USING entry (PG's spelling
9696            // lists only the extra sources). Peel it so the source subquery
9697            // does not re-scan — and shadow — the target table.
9698            let primary_is_target =
9699                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9700            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9701                let head = fc.joins.remove(0);
9702                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9703                mysql_on = head.on;
9704                fc = FromClause {
9705                    primary: head.table,
9706                    joins: fc.joins,
9707                };
9708            }
9709            Some(fc)
9710        } else {
9711            None
9712        };
9713        let where_ = if matches!(self.peek(), Token::Where) {
9714            self.advance();
9715            Some(self.parse_expr(0)?)
9716        } else {
9717            None
9718        };
9719        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9720        // read before RETURNING (MariaDB's own extension trails the LIMIT).
9721        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9722        let mut returning = self.parse_optional_returning()?;
9723        let where_ = if let Some(fc) = using_clause {
9724            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9725            // a USING-table reference in RETURNING becomes a correlated
9726            // scalar subquery over the USING list.
9727            let names: Vec<String> = core::iter::once(&fc.primary)
9728                .chain(fc.joins.iter().map(|j| &j.table))
9729                .flat_map(|t| {
9730                    t.alias
9731                        .clone()
9732                        .into_iter()
9733                        .chain(core::iter::once(t.name.clone()))
9734                })
9735                .collect();
9736            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9737            // join filters the SOURCE subquery on the ON predicate alone and
9738            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9739            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9740            // rows); every other form folds ON and WHERE into one EXISTS.
9741            let sub_where = match (mysql_on.clone(), where_.clone()) {
9742                _ if mysql_outer => mysql_on.clone(),
9743                (Some(on), Some(w)) => Some(Expr::Binary {
9744                    lhs: Box::new(on),
9745                    op: crate::ast::BinOp::And,
9746                    rhs: Box::new(w),
9747                }),
9748                (Some(on), None) => Some(on),
9749                (None, w) => w,
9750            };
9751            let exists_where = sub_where.clone();
9752            let sub_fc = fc.clone();
9753            let make_subq = move |leaf: Expr| -> Expr {
9754                Expr::ScalarSubquery(Box::new(SelectStatement {
9755                    locking: None,
9756                    ctes: Vec::new(),
9757                    distinct: false,
9758                    distinct_on: Vec::new(),
9759                    items: alloc::vec![SelectItem::Expr {
9760                        expr: leaf,
9761                        alias: None,
9762                    }],
9763                    from: Some(sub_fc.clone()),
9764                    where_: sub_where.clone(),
9765                    group_by: None,
9766                    group_by_all: false,
9767                    having: None,
9768                    unions: Vec::new(),
9769                    order_by: Vec::new(),
9770                    limit: None,
9771                    offset: None,
9772                    limit_with_ties: false,
9773                    window_check_exprs: Vec::new(),
9774                }))
9775            };
9776            let refs = |e: &Expr| expr_refs_tables(e, &names);
9777            if let Some(items) = returning.as_mut() {
9778                for item in items.iter_mut() {
9779                    if let SelectItem::Expr { expr, .. } = item {
9780                        wrap_from_leaves(expr, &names, &make_subq, &refs);
9781                    }
9782                }
9783            }
9784            // A LEFT join deletes the target rows the WHERE selects, reading
9785            // source columns through the correlated subquery (NULL when
9786            // unmatched); no EXISTS row filter.
9787            if mysql_outer {
9788                let mut outer = where_;
9789                if let Some(w) = outer.as_mut() {
9790                    wrap_from_leaves(w, &names, &make_subq, &refs);
9791                }
9792                outer
9793            } else {
9794                Some(Expr::Exists {
9795                    subquery: Box::new(SelectStatement {
9796                        locking: None,
9797                        ctes: Vec::new(),
9798                        distinct: false,
9799                        distinct_on: Vec::new(),
9800                        items: alloc::vec![SelectItem::Expr {
9801                            expr: Expr::Literal(Literal::Integer(1)),
9802                            alias: None,
9803                        }],
9804                        from: Some(fc),
9805                        where_: exists_where,
9806                        group_by: None,
9807                        group_by_all: false,
9808                        having: None,
9809                        unions: Vec::new(),
9810                        order_by: Vec::new(),
9811                        limit: None,
9812                        offset: None,
9813                        limit_with_ties: false,
9814                        window_check_exprs: Vec::new(),
9815                    }),
9816                    negated: false,
9817                })
9818            }
9819        } else {
9820            where_
9821        };
9822        Ok(Statement::Delete(crate::ast::DeleteStatement {
9823            ctes: Vec::new(),
9824            table,
9825            only,
9826            alias,
9827            where_,
9828            order_limit: delete_order_limit,
9829            returning,
9830        }))
9831    }
9832
9833    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9834    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9835    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9836    /// keyword. v7.17 surface:
9837    ///   * source: table reference (subquery source is a follow-up)
9838    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9839    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9840    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
9841    ///     order
9842    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9843        // INTO
9844        let is_into_kw = matches!(self.peek(), Token::Into)
9845            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9846        if !is_into_kw {
9847            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9848        }
9849        self.advance();
9850        let target = self.expect_ident_like()?;
9851        // Optional alias — bare ident before USING.
9852        let target_alias = match self.peek() {
9853            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9854                Some(self.expect_ident_like()?)
9855            }
9856            _ => None,
9857        };
9858        // USING
9859        let is_using_kw = matches!(
9860            self.peek(),
9861            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9862        );
9863        if !is_using_kw {
9864            return Err(self.err(format!(
9865                "expected USING after MERGE INTO target, got {:?}",
9866                self.peek()
9867            )));
9868        }
9869        self.advance();
9870        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9871        // <table> [alias]`. PG requires an alias after a subquery source.
9872        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9873            self.advance(); // (
9874            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9875            // constant-SELECT lowering the derived-table parser uses
9876            // (PG deletes through this form; it was a parse error).
9877            let inner = if matches!(self.peek(), Token::Values) {
9878                self.advance(); // VALUES
9879                Statement::Select(self.parse_values_rows_body()?)
9880            } else {
9881                self.parse_select_stmt()?
9882            };
9883            match self.advance() {
9884                Token::RParen => {}
9885                other => {
9886                    return Err(self.err(format!(
9887                        "expected ')' after MERGE USING subquery, got {other:?}"
9888                    )));
9889                }
9890            }
9891            let Statement::Select(sub) = inner else {
9892                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9893            };
9894            (String::new(), Some(Box::new(sub)))
9895        } else {
9896            (self.expect_ident_like()?, None)
9897        };
9898        let source_alias = match self.peek() {
9899            Token::Ident(s) | Token::QuotedIdent(s)
9900                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9901            {
9902                Some(self.expect_ident_like()?)
9903            }
9904            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9905                self.advance(); // AS
9906                Some(self.expect_ident_like()?)
9907            }
9908            _ => None,
9909        };
9910        // v7.39 (round 768, F31-D5) — optional positional column-alias
9911        // list after the source alias (`s(id, v)`).
9912        let mut source_column_aliases: Vec<String> = Vec::new();
9913        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9914            self.advance();
9915            loop {
9916                source_column_aliases.push(self.expect_ident_like()?);
9917                match self.peek() {
9918                    Token::Comma => {
9919                        self.advance();
9920                    }
9921                    Token::RParen => {
9922                        self.advance();
9923                        break;
9924                    }
9925                    other => {
9926                        return Err(self.err(format!(
9927                            "expected ',' or ')' in MERGE source column list, got {other:?}"
9928                        )));
9929                    }
9930                }
9931            }
9932        }
9933        if source_select.is_some() && source_alias.is_none() {
9934            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9935        }
9936        // ON
9937        if !matches!(self.peek(), Token::On) {
9938            return Err(self.err(format!(
9939                "expected ON after MERGE … USING source, got {:?}",
9940                self.peek()
9941            )));
9942        }
9943        self.advance();
9944        let on = self.parse_expr(0)?;
9945        // One or more WHEN clauses.
9946        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9947        loop {
9948            let is_when_kw = matches!(
9949                self.peek(),
9950                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9951            );
9952            if !is_when_kw {
9953                break;
9954            }
9955            self.advance(); // WHEN
9956            // [NOT] MATCHED
9957            let matched = if matches!(self.peek(), Token::Not) {
9958                self.advance();
9959                crate::ast::MergeMatched::NotMatched
9960            } else {
9961                crate::ast::MergeMatched::Matched
9962            };
9963            let is_matched_kw = matches!(
9964                self.peek(),
9965                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9966            );
9967            if !is_matched_kw {
9968                return Err(self.err(format!(
9969                    "expected MATCHED in WHEN clause, got {:?}",
9970                    self.peek()
9971                )));
9972            }
9973            self.advance();
9974            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9975            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9976            // to fire for target rows no source row matches.
9977            let mut matched = matched;
9978            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9979                self.advance();
9980                match self.peek() {
9981                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9982                        self.advance();
9983                        matched = crate::ast::MergeMatched::NotMatchedBySource;
9984                    }
9985                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9986                        self.advance();
9987                    }
9988                    other => {
9989                        return Err(self.err(format!(
9990                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9991                        )));
9992                    }
9993                }
9994            }
9995            // Optional AND <expr>
9996            let condition = if matches!(self.peek(), Token::And) {
9997                self.advance();
9998                Some(self.parse_expr(0)?)
9999            } else {
10000                None
10001            };
10002            // THEN
10003            let is_then_kw = matches!(
10004                self.peek(),
10005                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10006            );
10007            if !is_then_kw {
10008                return Err(self.err(format!(
10009                    "expected THEN in WHEN clause, got {:?}",
10010                    self.peek()
10011                )));
10012            }
10013            self.advance();
10014            // Action: INSERT / UPDATE / DELETE / DO NOTHING
10015            let action = match self.peek().clone() {
10016                Token::Insert => {
10017                    self.advance();
10018                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10019                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10020                    // VALUES (…)` omits it and fills every column in declaration
10021                    // order. PG accepts this; SPG used to require the list.
10022                    let mut columns: Vec<String> = Vec::new();
10023                    if matches!(self.peek(), Token::LParen) {
10024                        self.advance();
10025                        loop {
10026                            columns.push(self.expect_ident_like()?);
10027                            if matches!(self.peek(), Token::Comma) {
10028                                self.advance();
10029                                continue;
10030                            }
10031                            break;
10032                        }
10033                        if !matches!(self.peek(), Token::RParen) {
10034                            return Err(self.err(format!(
10035                                "expected ')' after INSERT column list, got {:?}",
10036                                self.peek()
10037                            )));
10038                        }
10039                        self.advance();
10040                    }
10041                    // VALUES (...)
10042                    if !matches!(self.peek(), Token::Values) {
10043                        return Err(self.err(format!(
10044                            "expected VALUES in MERGE INSERT, got {:?}",
10045                            self.peek()
10046                        )));
10047                    }
10048                    self.advance();
10049                    if !matches!(self.peek(), Token::LParen) {
10050                        return Err(self.err(format!(
10051                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
10052                            self.peek()
10053                        )));
10054                    }
10055                    self.advance();
10056                    let mut values: Vec<crate::ast::Expr> = Vec::new();
10057                    loop {
10058                        values.push(self.parse_expr(0)?);
10059                        if matches!(self.peek(), Token::Comma) {
10060                            self.advance();
10061                            continue;
10062                        }
10063                        break;
10064                    }
10065                    if !matches!(self.peek(), Token::RParen) {
10066                        return Err(self.err(format!(
10067                            "expected ')' after MERGE INSERT values, got {:?}",
10068                            self.peek()
10069                        )));
10070                    }
10071                    self.advance();
10072                    // Empty column list = positional into every column, so the
10073                    // count is checked against the table arity at execution.
10074                    if !columns.is_empty() && columns.len() != values.len() {
10075                        return Err(self.err(format!(
10076                            "MERGE INSERT column count ({}) ≠ value count ({})",
10077                            columns.len(),
10078                            values.len()
10079                        )));
10080                    }
10081                    crate::ast::MergeAction::Insert { columns, values }
10082                }
10083                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10084                    self.advance();
10085                    // SET
10086                    let is_set_kw = matches!(
10087                        self.peek(),
10088                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10089                    );
10090                    if !is_set_kw {
10091                        return Err(self.err(format!(
10092                            "expected SET after UPDATE in MERGE, got {:?}",
10093                            self.peek()
10094                        )));
10095                    }
10096                    self.advance();
10097                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10098                    loop {
10099                        let col = self.expect_ident_like()?;
10100                        if !matches!(self.peek(), Token::Eq) {
10101                            return Err(self.err(format!(
10102                                "expected '=' in MERGE UPDATE assignment, got {:?}",
10103                                self.peek()
10104                            )));
10105                        }
10106                        self.advance();
10107                        let expr = self.parse_expr(0)?;
10108                        assignments.push((col, expr));
10109                        if matches!(self.peek(), Token::Comma) {
10110                            self.advance();
10111                            continue;
10112                        }
10113                        break;
10114                    }
10115                    crate::ast::MergeAction::Update { assignments }
10116                }
10117                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10118                    self.advance();
10119                    crate::ast::MergeAction::Delete
10120                }
10121                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10122                    self.advance();
10123                    let is_nothing_kw = matches!(
10124                        self.peek(),
10125                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10126                    );
10127                    if !is_nothing_kw {
10128                        return Err(self.err(format!(
10129                            "expected NOTHING after DO in MERGE clause, got {:?}",
10130                            self.peek()
10131                        )));
10132                    }
10133                    self.advance();
10134                    crate::ast::MergeAction::DoNothing
10135                }
10136                other => {
10137                    return Err(self.err(format!(
10138                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10139                    )));
10140                }
10141            };
10142            // PG's grammar simply has no INSERT production under BY SOURCE
10143            // (a target row already exists there) — same syntax error.
10144            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10145                && matches!(action, crate::ast::MergeAction::Insert { .. })
10146            {
10147                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10148            }
10149            clauses.push(crate::ast::MergeWhenClause {
10150                matched,
10151                condition,
10152                action,
10153            });
10154        }
10155        if clauses.is_empty() {
10156            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10157        }
10158        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10159        // unconditional (no `AND`) WHEN of the same match kind: it could
10160        // never fire. Check per match kind in clause order.
10161        let mut seen_unconditional_matched = false;
10162        let mut seen_unconditional_not_matched = false;
10163        let mut seen_unconditional_by_source = false;
10164        for c in &clauses {
10165            let seen = match c.matched {
10166                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10167                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10168                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10169            };
10170            if *seen {
10171                return Err(self.err(String::from(
10172                    "unreachable WHEN clause specified after unconditional WHEN clause",
10173                )));
10174            }
10175            if c.condition.is_none() {
10176                *seen = true;
10177            }
10178        }
10179        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10180        let returning = self.parse_optional_returning()?;
10181        Ok(Statement::Merge(crate::ast::MergeStatement {
10182            // Attached by `parse_with_cte_then_select` when the MERGE
10183            // heads a WITH clause (round 149).
10184            ctes: Vec::new(),
10185            target,
10186            target_alias,
10187            source,
10188            source_alias,
10189            source_select,
10190            source_column_aliases,
10191            on,
10192            clauses,
10193            returning,
10194        }))
10195    }
10196
10197    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10198    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10199    /// as SELECT, so `RETURNING *`, `RETURNING col`,
10200    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10201    fn parse_optional_returning(
10202        &mut self,
10203    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10204        let is_returning_kw = matches!(
10205            self.peek(),
10206            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10207        );
10208        if !is_returning_kw {
10209            return Ok(None);
10210        }
10211        self.advance();
10212        let mut items = Vec::new();
10213        loop {
10214            items.push(self.parse_select_item()?);
10215            if matches!(self.peek(), Token::Comma) {
10216                self.advance();
10217                continue;
10218            }
10219            break;
10220        }
10221        Ok(Some(items))
10222    }
10223
10224    /// v6.0.4 — parse the tail of an ALTER statement after the
10225    /// leading `ALTER` keyword has been consumed. Only one form is
10226    /// supported in v6.0.4:
10227    ///
10228    /// ```text
10229    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10230    /// ```
10231    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10232        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10233        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10234        // exclusion) is accepted by stripping the `ONLY` keyword
10235        // before the table parse.
10236        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10237        // and the long PG-dump tail are accepted as no-ops.
10238        match self.advance() {
10239            Token::Index => {}
10240            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10241            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10242            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10243            Token::Table => {
10244                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10245                    self.advance();
10246                }
10247                return self.parse_alter_table_after_keyword();
10248            }
10249            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10250                return self.parse_alter_policy_after_keyword();
10251            }
10252            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10253                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10254                    self.advance();
10255                }
10256                return self.parse_alter_table_after_keyword();
10257            }
10258            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10259            // of the silent-noop tail.
10260            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10261                return self.parse_alter_sequence_after_keyword();
10262            }
10263            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10264            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10265            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10266            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10267                // NB: the match arm consumed `TYPE` via self.advance(); the
10268                // cursor is now at the type name — do NOT advance again.
10269                let type_name = self.expect_ident_like()?;
10270                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10271                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10272                if is_add_value {
10273                    self.advance(); // ADD
10274                    self.advance(); // VALUE
10275                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10276                    // IF/EXISTS as identifiers.
10277                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10278                    {
10279                        let n1 = self.tokens.get(self.pos + 1);
10280                        let n2 = self.tokens.get(self.pos + 2);
10281                        if matches!(n1, Some(Token::Not))
10282                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10283                        {
10284                            self.advance();
10285                            self.advance();
10286                            self.advance();
10287                            true
10288                        } else {
10289                            false
10290                        }
10291                    } else {
10292                        false
10293                    };
10294                    let label = self.expect_string_literal()?;
10295                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10296                    {
10297                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10298                        self.advance();
10299                        let anchor = self.expect_string_literal()?;
10300                        Some((is_before, anchor))
10301                    } else {
10302                        None
10303                    };
10304                    return Ok(Statement::AlterTypeAddValue {
10305                        type_name,
10306                        label,
10307                        if_not_exists,
10308                        position,
10309                    });
10310                }
10311                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10312                // Used to fall into the no-op tail below: accepted, silently
10313                // ignored. `RENAME TO <newtype>` keeps falling through.
10314                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10315                    && matches!(
10316                        self.tokens.get(self.pos + 1),
10317                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10318                    )
10319                {
10320                    self.advance(); // RENAME
10321                    self.advance(); // VALUE
10322                    let old = self.expect_string_literal()?;
10323                    if matches!(self.peek(), Token::To) {
10324                        self.advance();
10325                    } else {
10326                        self.expect_keyword_ident("to")?;
10327                    }
10328                    let new = self.expect_string_literal()?;
10329                    return Ok(Statement::AlterTypeRenameValue {
10330                        type_name,
10331                        old,
10332                        new,
10333                    });
10334                }
10335                // Other ALTER TYPE forms — the ACTION stays a no-op
10336                // (pg_dump tail), but v7.39 (round 708) the NAME is
10337                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10338                // success for a type that does not exist.
10339                self.consume_until_statement_boundary();
10340                return Ok(Statement::ValidateOnly {
10341                    kind: crate::ast::ValidateOnlyKind::TypeName,
10342                    names: alloc::vec![type_name],
10343                });
10344            }
10345            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10346            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10347            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10348            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10349            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10350            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10351            // pg_dump no-op list below: every form used to report success
10352            // and change nothing, which is worse than refusing outright
10353            // (a migration dropping a constraint kept rejecting data).
10354            // NOTE: the enclosing `match self.advance()` already consumed
10355            // the DOMAIN keyword, so the name is next.
10356            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10357                return self.parse_alter_domain_after_keyword();
10358            }
10359            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10360            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10361            // used to fall into the pg_dump no-op tail below, so a DBA
10362            // setting a per-role default was told it worked and nothing
10363            // happened. Intercepted here, BEFORE that tail.
10364            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10365            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10366            // interception below exists: swallowed with the no-op tail, an
10367            // unknown parameter name was ACCEPTED where PG18 answers
10368            // `unrecognized configuration parameter`. SPG applies nothing
10369            // either way — there is no postgresql.auto.conf — but it now
10370            // says so about a name it does not know.
10371            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10372                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10373                // already consumed here. An extra advance eats the SET and
10374                // the parameter name is never seen — which is exactly the
10375                // bug a panic in this branch disproved: the branch WAS on
10376                // the path, the reading of it was wrong.
10377                let mut parameter = None;
10378                // SET <name> … | RESET <name> | RESET ALL
10379                if matches!(self.peek(), Token::Ident(k)
10380                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10381                {
10382                    self.advance();
10383                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10384                        && !n.eq_ignore_ascii_case("all")
10385                    {
10386                        self.advance();
10387                        // A dotted GUC (`plpgsql.check_asserts`) is two
10388                        // tokens; keep the whole name.
10389                        let mut full = n;
10390                        while matches!(self.peek(), Token::Dot) {
10391                            self.advance();
10392                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10393                                full.push('.');
10394                                full.push_str(&t);
10395                            }
10396                        }
10397                        parameter = Some(full);
10398                    }
10399                }
10400                self.consume_until_statement_boundary();
10401                return Ok(Statement::AlterSystem { parameter });
10402            }
10403            Token::Ident(s) | Token::QuotedIdent(s)
10404                if matches!(
10405                    s.to_ascii_lowercase().as_str(),
10406                    "role" | "user" | "database"
10407                ) && self.peeks_db_role_setting() =>
10408            {
10409                let is_database = s.eq_ignore_ascii_case("database");
10410                return self.parse_db_role_setting(is_database);
10411            }
10412            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10413            // (the non-SET forms; SET/RESET took the branch above). The
10414            // attributes still no-op — recorded, and the ignored PASSWORD
10415            // is ledgered as its own follow-up — but the ROLE is validated:
10416            // any name was accepted for a role that does not exist.
10417            Token::Ident(s) | Token::QuotedIdent(s)
10418                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10419            {
10420                // NB: the enclosing `match self.advance()` already consumed
10421                // ROLE/USER — the round-695 trap, hit again in this round's
10422                // first draft (the name was eaten and WITH parsed as the
10423                // role). The cursor is at the name.
10424                let name = self.expect_ident_or_string()?;
10425                // v7.39 (round 750) — scan the attribute tail for
10426                // PASSWORD. Everything else stays a recorded no-op, but
10427                // a dropped credential rotation is a SECURITY bug:
10428                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10429                // changed nothing, so the old password kept working.
10430                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10431                // NULL` clears the credential.
10432                let mut password: Option<Option<String>> = None;
10433                loop {
10434                    match self.peek() {
10435                        Token::Semicolon | Token::Eof => break,
10436                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10437                            self.advance();
10438                            match self.advance() {
10439                                Token::String(p) => password = Some(Some(p)),
10440                                Token::Null => password = Some(None),
10441                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10442                                    password = Some(None);
10443                                }
10444                                other => {
10445                                    return Err(self.err(alloc::format!(
10446                                        "expected password string or NULL after PASSWORD, got {other:?}"
10447                                    )));
10448                                }
10449                            }
10450                        }
10451                        _ => {
10452                            self.advance();
10453                        }
10454                    }
10455                }
10456                if name.eq_ignore_ascii_case("all") {
10457                    // `ALTER ROLE ALL …` names every role; nothing to check.
10458                    return Ok(Statement::Empty);
10459                }
10460                if let Some(pw) = password {
10461                    return Ok(Statement::AlterRolePassword { name, password: pw });
10462                }
10463                return Ok(Statement::ValidateOnly {
10464                    kind: crate::ast::ValidateOnlyKind::RoleName,
10465                    names: alloc::vec![name],
10466                });
10467            }
10468            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10469            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10470            // list far enough to validate the NAME; the actions still no-op.
10471            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10472            // models none of them and their dumps are rare.)
10473            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10474                let name = self.expect_ident_or_string()?;
10475                self.consume_until_statement_boundary();
10476                return Ok(Statement::ValidateOnly {
10477                    kind: crate::ast::ValidateOnlyKind::CollationName,
10478                    names: alloc::vec![name],
10479                });
10480            }
10481            Token::Ident(s) | Token::QuotedIdent(s)
10482                if s.eq_ignore_ascii_case("text")
10483                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10484                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10485            {
10486                self.advance(); // SEARCH
10487                self.advance(); // CONFIGURATION
10488                let name = self.expect_ident_like()?;
10489                self.consume_until_statement_boundary();
10490                return Ok(Statement::ValidateOnly {
10491                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10492                    names: alloc::vec![name],
10493                });
10494            }
10495            Token::Ident(s) | Token::QuotedIdent(s)
10496                if s.eq_ignore_ascii_case("event")
10497                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10498            {
10499                self.advance(); // TRIGGER
10500                let name = self.expect_ident_like()?;
10501                self.consume_until_statement_boundary();
10502                return Ok(Statement::ValidateOnly {
10503                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10504                    names: alloc::vec![name],
10505                });
10506            }
10507            Token::Ident(s) | Token::QuotedIdent(s)
10508                if s.eq_ignore_ascii_case("large")
10509                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10510            {
10511                self.advance(); // OBJECT
10512                let oid = match self.advance() {
10513                    Token::Integer(n) => alloc::format!("{n}"),
10514                    other => {
10515                        return Err(
10516                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10517                        );
10518                    }
10519                };
10520                self.consume_until_statement_boundary();
10521                return Ok(Statement::ValidateOnly {
10522                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10523                    names: alloc::vec![oid],
10524                });
10525            }
10526            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10527            // argument-list parse as DROP AGGREGATE (round 707); the
10528            // action no-ops, the existence check is real.
10529            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10530                // Same round-695 trap as above: AGGREGATE is already
10531                // consumed; the cursor is at the name.
10532                let name = self.expect_ident_like()?;
10533                let mut names = alloc::vec![name];
10534                if matches!(self.peek(), Token::LParen) {
10535                    self.advance();
10536                    loop {
10537                        match self.peek().clone() {
10538                            Token::RParen => {
10539                                self.advance();
10540                                break;
10541                            }
10542                            Token::Star => {
10543                                self.advance();
10544                                names.push(String::from("*"));
10545                            }
10546                            Token::Comma => {
10547                                self.advance();
10548                            }
10549                            _ => {
10550                                let mut t = self.expect_ident_like()?;
10551                                while let Token::Ident(nx) = self.peek() {
10552                                    let nx = nx.clone();
10553                                    self.advance();
10554                                    t.push(' ');
10555                                    t.push_str(&nx);
10556                                }
10557                                names.push(t);
10558                            }
10559                        }
10560                    }
10561                }
10562                self.consume_until_statement_boundary();
10563                return Ok(Statement::ValidateOnly {
10564                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10565                    names,
10566                });
10567            }
10568            Token::Ident(s) | Token::QuotedIdent(s)
10569                if matches!(
10570                    s.to_ascii_lowercase().as_str(),
10571                    "view"
10572                        | "function"
10573                        | "database"
10574                        | "schema"
10575                        | "owner"
10576                        | "default"
10577                        | "extension"
10578                        | "materialized"
10579                        | "publication"
10580                        | "subscription"
10581                        // v7.37.17 (17.6 siblings) — additional ALTER
10582                        // targets pg_dump / pg_dumpall / operator DB
10583                        // migration scripts commonly emit. SPG has
10584                        // no matching machinery for any of these; the
10585                        // parser accepts + Empty-returns so pg_dump
10586                        // tail statements don't stall.
10587                        | "tablespace"
10588                        | "language"
10589                        | "operator"
10590                        | "conversion"
10591                        | "statistics"
10592                        | "server"
10593                        | "foreign"
10594                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10595                        // / TEMPLATE (CONFIGURATION intercepted above).
10596                        | "text"
10597                ) =>
10598            {
10599                self.consume_until_statement_boundary();
10600                return Ok(Statement::Empty);
10601            }
10602            other => {
10603                return Err(self.err(format!(
10604                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10605                     after ALTER, got {other:?}"
10606                )));
10607            }
10608        }
10609        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10610        // (mailrs migrate-042 ships these). The presence of an
10611        // IF EXISTS makes the subsequent name lookup tolerate
10612        // a missing index — engine returns CommandOk no-op.
10613        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10614            let next = self.tokens.get(self.pos + 1);
10615            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10616                self.advance();
10617                self.advance();
10618                true
10619            } else {
10620                false
10621            }
10622        } else {
10623            false
10624        };
10625        let name = self.expect_ident_like()?;
10626        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10627        // Detect BEFORE the REBUILD path so the existing REBUILD
10628        // arm stays untouched.
10629        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10630            self.advance();
10631            if matches!(self.peek(), Token::To) {
10632                self.advance();
10633            } else {
10634                self.expect_keyword_ident("to")?;
10635            }
10636            let new = self.expect_ident_like()?;
10637            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10638                name,
10639                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10640            }));
10641        }
10642        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10643        // A syntax error before; the index is validated, the params no-op.
10644        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10645            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10646                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10647        {
10648            self.consume_until_statement_boundary();
10649            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10650                name,
10651                target: crate::ast::AlterIndexTarget::StorageParams,
10652            }));
10653        }
10654        // REBUILD
10655        self.expect_keyword_ident("rebuild")?;
10656        // Optional: WITH (encoding = <enc>)
10657        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10658            self.advance();
10659            if !matches!(self.peek(), Token::LParen) {
10660                return Err(self.err(format!(
10661                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10662                    self.peek()
10663                )));
10664            }
10665            self.advance();
10666            self.expect_keyword_ident("encoding")?;
10667            if !matches!(self.peek(), Token::Eq) {
10668                return Err(self.err(format!(
10669                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10670                    self.peek()
10671                )));
10672            }
10673            self.advance();
10674            let enc_ident = match self.advance() {
10675                Token::Ident(s) | Token::QuotedIdent(s) => s,
10676                other => {
10677                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10678                }
10679            };
10680            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10681                "f32" => VecEncoding::F32,
10682                "sq8" => VecEncoding::Sq8,
10683                "half" => VecEncoding::F16,
10684                other => {
10685                    return Err(self.err(format!(
10686                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10687                    )));
10688                }
10689            };
10690            if !matches!(self.peek(), Token::RParen) {
10691                return Err(self.err(format!(
10692                    "expected ')' after encoding value, got {:?}",
10693                    self.peek()
10694                )));
10695            }
10696            self.advance();
10697            Some(enc)
10698        } else {
10699            None
10700        };
10701        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10702            name,
10703            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10704        }))
10705    }
10706
10707    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10708    /// only `SET` form currently supported; future v6.7.x can add
10709    /// more SET subjects without changing the dispatch shape.
10710    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10711    /// subactions. Single-subaction shape stays a 1-element vec.
10712    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10713        let table_name = self.expect_ident_like()?;
10714        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10715        loop {
10716            let subaction = self.parse_alter_table_subaction()?;
10717            // ADD COLUMN with inline REFERENCES emits both an
10718            // AddColumn and an AddForeignKey subaction; the
10719            // helper returns 1 or 2 items.
10720            targets.extend(subaction);
10721            if matches!(self.peek(), Token::Comma) {
10722                self.advance();
10723                continue;
10724            }
10725            break;
10726        }
10727        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10728            name: table_name,
10729            targets,
10730        }))
10731    }
10732
10733    /// Parse one ALTER TABLE subaction. Returns a Vec because
10734    /// inline `REFERENCES` on `ADD COLUMN` produces both an
10735    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10736    fn parse_alter_table_subaction(
10737        &mut self,
10738    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10739        match self.peek() {
10740            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10741                self.advance();
10742                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10743                // storage parameters: paren-prefixed; consume.
10744                if matches!(self.peek(), Token::LParen) {
10745                    self.consume_until_statement_boundary();
10746                    return Ok(Vec::new());
10747                }
10748                let setting = self.expect_ident_like()?;
10749                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10750                    if !matches!(self.peek(), Token::Eq) {
10751                        return Err(self.err(alloc::format!(
10752                            "expected '=' after hot_tier_bytes, got {:?}",
10753                            self.peek()
10754                        )));
10755                    }
10756                    self.advance();
10757                    let n = self.expect_u64_literal()?;
10758                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10759                }
10760                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10761                // accept-and-no-op for ALTER TABLE SET <subject>
10762                // forms that pg_dump emits but SPG either treats
10763                // as N/A (single-tenant, single-owner, no shared
10764                // tablespaces) or accepts the dump-side declaration
10765                // without runtime effect:
10766                //   SET SCHEMA <name>            (18.11)
10767                //   SET TABLESPACE <name>        (18.8)
10768                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
10769                //   SET WITHOUT CLUSTER          (18.13)
10770                //   SET WITHOUT OIDS             (PG legacy)
10771                //   SET (option = value, …)      (storage parameters)
10772                //   SET REPLICA IDENTITY {…}     (18.14)
10773                if setting.eq_ignore_ascii_case("schema")
10774                    || setting.eq_ignore_ascii_case("tablespace")
10775                    || setting.eq_ignore_ascii_case("logged")
10776                    || setting.eq_ignore_ascii_case("unlogged")
10777                    || setting.eq_ignore_ascii_case("without")
10778                {
10779                    self.consume_until_statement_boundary();
10780                    return Ok(Vec::new());
10781                }
10782                if setting.eq_ignore_ascii_case("replica") {
10783                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10784                    self.consume_until_statement_boundary();
10785                    return Ok(Vec::new());
10786                }
10787                // SET (option=value, …) — storage parameters.
10788                if matches!(self.peek(), Token::LParen) {
10789                    self.consume_until_statement_boundary();
10790                    return Ok(Vec::new());
10791                }
10792                Err(self.err(alloc::format!(
10793                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10794                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10795                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10796                )))
10797            }
10798            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10799            // not ignored: round 645 gave SPG the inheritance the
10800            // v7.37.18 no-op said it did not have.
10801            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10802                self.advance();
10803                let parent = self.expect_ident_like()?;
10804                self.consume_until_statement_boundary();
10805                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10806                    parent,
10807                    detach: false
10808                }])
10809            }
10810            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10811            // LEVEL SECURITY`, which has its own RLS arm below — without
10812            // the guard this swallowed NO FORCE as a no-op.
10813            Token::Ident(s)
10814                if s.eq_ignore_ascii_case("no")
10815                    && !matches!(
10816                        self.tokens.get(self.pos + 1),
10817                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10818                    ) =>
10819            {
10820                self.advance();
10821                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10822                    if k.eq_ignore_ascii_case("inherit"))
10823                {
10824                    self.advance();
10825                    let parent = self.expect_ident_like()?;
10826                    self.consume_until_statement_boundary();
10827                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10828                        parent,
10829                        detach: true
10830                    }]);
10831                }
10832                self.consume_until_statement_boundary();
10833                Ok(Vec::new())
10834            }
10835            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10836            // single-owner, so there is still nothing to record.
10837            //
10838            // v7.39 (round 652) — but the name now reaches the engine,
10839            // which refuses a role that does not exist as PG does. The
10840            // no-op was swallowing the whole statement, so a dump naming
10841            // a role this server never heard of restored clean and left
10842            // the table owned by whoever ran the restore.
10843            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10844                self.advance();
10845                if matches!(self.peek(), Token::To) {
10846                    self.advance();
10847                }
10848                let role = self.expect_ident_like()?;
10849                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10850                    role
10851                }])
10852            }
10853            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10854            // PG sets a hint; SPG doesn't have clustered storage, so the
10855            // hint itself stays a no-op.
10856            //
10857            // v7.39 (round 652) — the index name is checked now. PG
10858            // errors on one that does not exist, and swallowing that let
10859            // a typo'd CLUSTER ON pass silently.
10860            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10861                self.advance();
10862                // `ON` is a reserved token, not an ident.
10863                if !matches!(self.peek(), Token::On) {
10864                    return Err(self.err(alloc::format!(
10865                        "expected ON after CLUSTER, got {:?}",
10866                        self.peek()
10867                    )));
10868                }
10869                self.advance();
10870                let index = self.expect_ident_like()?;
10871                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10872                    index: Some(index)
10873                }])
10874            }
10875            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10876            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10877            // what a logical decoder puts in the old-tuple image; SPG's
10878            // replication is SQL-text, so there is nothing to record.
10879            // Accept-and-no-op (it used to be a parse error).
10880            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10881                self.advance();
10882                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10883                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10884                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10885                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10886                {
10887                    self.advance(); // IDENTITY
10888                    self.advance(); // USING
10889                    if matches!(self.peek(), Token::Index)
10890                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10891                    {
10892                        self.advance();
10893                    }
10894                    let index = self.expect_ident_like()?;
10895                    self.consume_until_statement_boundary();
10896                    return Ok(alloc::vec![
10897                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10898                    ]);
10899                }
10900                self.consume_until_statement_boundary();
10901                Ok(Vec::new())
10902            }
10903            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10904            //
10905            // v7.39 (round 652) — it used to consume the statement and
10906            // return nothing, on the stated theory that SPG validated at
10907            // ADD CONSTRAINT time so there was never anything left to
10908            // validate. Measured against PG18, ADD CONSTRAINT did not
10909            // scan the existing rows at all — the comment described a
10910            // property SPG did not have, which is why nobody looked. Both
10911            // halves are real now: ADD scans unless told NOT VALID, and
10912            // this scans what NOT VALID skipped.
10913            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10914                self.advance();
10915                self.expect_keyword_ident("constraint")?;
10916                let name = self.expect_ident_like()?;
10917                Ok(alloc::vec![
10918                    crate::ast::AlterTableTarget::ValidateConstraint { name }
10919                ])
10920            }
10921            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10922            // SET (option = value, …). PG uses it to clear per-table
10923            // storage params like fillfactor or autovacuum_*. SPG
10924            // engine-manages those parameters; accept-and-no-op.
10925            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10926                self.advance();
10927                self.consume_until_statement_boundary();
10928                Ok(Vec::new())
10929            }
10930            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10931            // type-of binding (PG 9.0+). SPG composite types
10932            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10933            // TABLE OF is rare and inverse of CREATE TABLE OF.
10934            // Accept-and-no-op until a customer dump round-trips it.
10935            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10936                self.advance();
10937                // v7.39 (round 710) — the type name is validated now.
10938                let type_name = self.expect_ident_like()?;
10939                self.consume_until_statement_boundary();
10940                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10941                    type_name
10942                }])
10943            }
10944            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10945            // (reserved keyword) rather than Token::Ident("not"),
10946            // so it needs its own arm. Accept-and-no-op same as OF.
10947            Token::Not => {
10948                self.advance();
10949                self.consume_until_statement_boundary();
10950                Ok(Vec::new())
10951            }
10952            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10953            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10954                self.advance();
10955                self.expect_row_level_security()?;
10956                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10957                    enabled: None,
10958                    force: Some(true),
10959                }])
10960            }
10961            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
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(); // NO
10970                self.advance(); // FORCE
10971                self.expect_row_level_security()?;
10972                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10973                    enabled: None,
10974                    force: Some(false),
10975                }])
10976            }
10977            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10978            // (sets relrowsecurity). The guard requires the next token to be
10979            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10980            Token::Ident(s)
10981                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10982                    && matches!(
10983                        self.tokens.get(self.pos + 1),
10984                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10985                    ) =>
10986            {
10987                let enabled = s.eq_ignore_ascii_case("enable");
10988                self.advance(); // ENABLE/DISABLE
10989                self.expect_row_level_security()?;
10990                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10991                    enabled: Some(enabled),
10992                    force: None,
10993                }])
10994            }
10995            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10996                self.advance();
10997                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10998                // {INDEX|KEY} [name] (cols)`, which every ORM migration
10999                // emits. The same grammar CREATE TABLE already accepts
11000                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11001                // through the SAME parser — an ALTER-only copy would be a
11002                // second place for the two to drift.
11003                if self.peek_mysql_inline_key_start() {
11004                    return Ok(match self.parse_mysql_inline_key()? {
11005                        Some(c) => {
11006                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11007                        }
11008                        // FULLTEXT / SPATIAL parse and are accepted as a
11009                        // no-op here exactly as they are inline.
11010                        None => Vec::new(),
11011                    });
11012                }
11013                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11014                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11015                // PRIMARY KEY this way; mysqldump emits both.
11016                // Peek-only dispatch (no advance) — `advance()`
11017                // destructively replaces consumed tokens with Eof,
11018                // so saved-pos restore would land on Eofs.
11019                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11020                {
11021                    // The next-but-one ident is the constraint
11022                    // name; the one after THAT is the kind.
11023                    let kind_pos = self.pos + 2;
11024                    let kind = self.tokens.get(kind_pos).cloned();
11025                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11026                    {
11027                        let fk = self.parse_table_level_fk()?;
11028                        return Ok(alloc::vec![
11029                            crate::ast::AlterTableTarget::AddForeignKey(fk)
11030                        ]);
11031                    }
11032                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11033                    {
11034                        self.advance(); // CONSTRAINT
11035                        // v7.39 (read01 round 48) — keep the name; the engine
11036                        // stores it now instead of dropping it on the floor.
11037                        let con_name = self.expect_ident_like()?;
11038                        self.advance(); // PRIMARY
11039                        self.expect_keyword_ident("key")?;
11040                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11041                        // v7.39 (round 711) — the ALTER form carries the
11042                        // timing too (pg_dump writes it here).
11043                        let (deferrable, initially_deferred) =
11044                            self.consume_deferrable_clauses_timed()?;
11045                        return Ok(alloc::vec![
11046                            crate::ast::AlterTableTarget::AddTableConstraint(
11047                                crate::ast::TableConstraint::PrimaryKey {
11048                                    name: Some(con_name),
11049                                    columns: cols,
11050                                    deferrable,
11051                                    initially_deferred,
11052                                }
11053                            )
11054                        ]);
11055                    }
11056                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11057                    {
11058                        self.advance(); // CONSTRAINT
11059                        // v7.39 (read01 round 48) — keep the name.
11060                        let con_name = self.expect_ident_like()?;
11061                        // v7.22 (mailrs round-13 gap 6) — delegate so
11062                        // the optional `NULLS [NOT] DISTINCT` modifier
11063                        // parses here too (pg_dump emits the ALTER
11064                        // form; semantics enforced by the engine
11065                        // since v7.13).
11066                        let mut uc = self.parse_table_level_unique()?;
11067                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11068                            *name = Some(con_name);
11069                        }
11070                        return Ok(alloc::vec![
11071                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11072                        ]);
11073                    }
11074                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11075                    {
11076                        self.advance(); // CONSTRAINT
11077                        // v7.39 (read01 round 48) — keep the name.
11078                        let con_name = self.expect_ident_like()?;
11079                        self.advance(); // CHECK
11080                        if !matches!(self.peek(), Token::LParen) {
11081                            return Err(self.err(alloc::format!(
11082                                "expected '(' after CHECK, got {:?}", self.peek()
11083                            )));
11084                        }
11085                        self.advance();
11086                        let expr = self.parse_expr(0)?;
11087                        if matches!(self.peek(), Token::RParen) {
11088                            self.advance();
11089                        }
11090                        let not_valid = self.parse_not_valid_suffix();
11091                        return Ok(alloc::vec![
11092                            crate::ast::AlterTableTarget::AddTableConstraint(
11093                                crate::ast::TableConstraint::Check {
11094                                    name: Some(con_name),
11095                                    expr,
11096                                    not_valid,
11097                                }
11098                            )
11099                        ]);
11100                    }
11101                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11102                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11103                    // exclusion constraints via this ALTER form.
11104                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11105                    {
11106                        self.advance(); // CONSTRAINT
11107                        let con_name = self.expect_ident_like()?;
11108                        let mut ex = self.parse_table_level_exclude()?;
11109                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11110                            *name = Some(con_name);
11111                        }
11112                        return Ok(alloc::vec![
11113                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11114                        ]);
11115                    }
11116                    // Unknown kind — fall through to FK path which
11117                    // produces a descriptive parse error.
11118                }
11119                let is_fk = matches!(
11120                    self.peek(),
11121                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11122                        || s.eq_ignore_ascii_case("foreign")
11123                );
11124                if is_fk {
11125                    let fk = self.parse_table_level_fk()?;
11126                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11127                }
11128                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11129                // (no CONSTRAINT prefix) — same dispatch.
11130                match self.peek().clone() {
11131                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11132                        self.advance();
11133                        self.expect_keyword_ident("key")?;
11134                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11135                        let (deferrable, initially_deferred) =
11136                            self.consume_deferrable_clauses_timed()?;
11137                        return Ok(alloc::vec![
11138                            crate::ast::AlterTableTarget::AddTableConstraint(
11139                                crate::ast::TableConstraint::PrimaryKey {
11140                                    name: None,
11141                                    columns: cols,
11142                                    deferrable,
11143                                    initially_deferred,
11144                                }
11145                            )
11146                        ]);
11147                    }
11148                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11149                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
11150                        let uc = self.parse_table_level_unique()?;
11151                        return Ok(alloc::vec![
11152                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11153                        ]);
11154                    }
11155                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11156                    // prefix). The other three bare forms were here and
11157                    // this one was not, so it fell through to the column
11158                    // path and came back as "unexpected reserved keyword
11159                    // 'check' at start of column definition".
11160                    _ if self.peek_table_level_check_start() => {
11161                        let chk = self.parse_table_level_check()?;
11162                        let not_valid = self.parse_not_valid_suffix();
11163                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11164                            unreachable!("parse_table_level_check returns Check")
11165                        };
11166                        return Ok(alloc::vec![
11167                            crate::ast::AlterTableTarget::AddTableConstraint(
11168                                crate::ast::TableConstraint::Check {
11169                                    name: None,
11170                                    expr,
11171                                    not_valid,
11172                                }
11173                            )
11174                        ]);
11175                    }
11176                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11177                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11178                        let ex = self.parse_table_level_exclude()?;
11179                        return Ok(alloc::vec![
11180                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11181                        ]);
11182                    }
11183                    _ => {}
11184                }
11185                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11186                    self.advance();
11187                }
11188                let mut if_not_exists = false;
11189                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11190                    self.advance();
11191                    if !matches!(self.peek(), Token::Not) {
11192                        return Err(self.err(alloc::format!(
11193                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11194                            self.peek()
11195                        )));
11196                    }
11197                    self.advance();
11198                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11199                        return Err(self.err(alloc::format!(
11200                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11201                            self.peek()
11202                        )));
11203                    }
11204                    self.advance();
11205                    if_not_exists = true;
11206                }
11207                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11208                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11209                // returns ColumnDef + an optional inline FK.
11210                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11211                let col_name = column.name.clone();
11212                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11213                    column,
11214                    if_not_exists,
11215                }];
11216                if let Some(mut fk) = col_level_fk {
11217                    if fk.columns.is_empty() {
11218                        fk.columns.push(col_name);
11219                    }
11220                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11221                }
11222                Ok(out)
11223            }
11224            Token::Drop => {
11225                self.advance();
11226                // v7.13.3 — dispatch on the next token. mailrs round-7
11227                // S8 closed DROP COLUMN; round-6 S7 closed
11228                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11229                // RESTRICT modifiers.
11230                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11231                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11232                let subject = match self.peek() {
11233                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11234                        self.advance();
11235                        "constraint"
11236                    }
11237                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11238                        self.advance();
11239                        "column"
11240                    }
11241                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11242                    // `INDEX` lexes as the reserved Token::Index, so it is
11243                    // unambiguous. `KEY` is a plain ident, and PG allows a
11244                    // column literally named "key", so only read it as the
11245                    // keyword when a name follows it.
11246                    Token::Index => {
11247                        self.advance();
11248                        "index"
11249                    }
11250                    Token::Ident(s)
11251                        if s.eq_ignore_ascii_case("key")
11252                            && matches!(
11253                                self.tokens.get(self.pos + 1),
11254                                Some(Token::Ident(_) | Token::QuotedIdent(_))
11255                            ) =>
11256                    {
11257                        self.advance();
11258                        "index"
11259                    }
11260                    // PG-canonical bare `DROP <col>` without COLUMN
11261                    // keyword is also valid; treat any other ident
11262                    // as the column name.
11263                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11264                    other => {
11265                        return Err(self.err(alloc::format!(
11266                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11267                        )));
11268                    }
11269                };
11270                let mut if_exists = false;
11271                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11272                    let n1 = self.tokens.get(self.pos + 1);
11273                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11274                        self.advance();
11275                        self.advance();
11276                        if_exists = true;
11277                    }
11278                }
11279                let name = self.expect_ident_like()?;
11280                let mut cascade = false;
11281                if matches!(
11282                    self.peek(),
11283                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11284                        || s.eq_ignore_ascii_case("restrict")
11285                ) {
11286                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11287                    {
11288                        cascade = true;
11289                    }
11290                    self.advance();
11291                }
11292                if subject == "index" {
11293                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11294                        name,
11295                        if_exists,
11296                    }])
11297                } else if subject == "constraint" {
11298                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11299                        name,
11300                        if_exists,
11301                    }])
11302                } else {
11303                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11304                        column: name,
11305                        if_exists,
11306                        cascade,
11307                    }])
11308                }
11309            }
11310            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11311                self.advance();
11312                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11313                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11314                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11315                // immediately; accept-and-no-op.
11316                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11317                    self.advance();
11318                    self.consume_until_statement_boundary();
11319                    return Ok(Vec::new());
11320                }
11321                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11322                    self.advance();
11323                }
11324                let col_name = self.expect_ident_like()?;
11325                match self.peek() {
11326                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11327                        self.advance();
11328                    }
11329                    // v7.14.0 — pg_dump emits BIGSERIAL via
11330                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11331                    // nextval('seq')` (the sequence is created
11332                    // separately). SPG's BIGSERIAL already uses
11333                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11334                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11335                    // engine no-ops by consuming the tail.
11336                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11337                        // v7.22 (round-13 T2) — `SET DEFAULT
11338                        // nextval('…')` is how pg_dump spells a
11339                        // SERIAL column (plain integer in CREATE
11340                        // TABLE + this ALTER). It used to be
11341                        // swallowed as a no-op, which silently
11342                        // STRIPPED auto-increment from imported
11343                        // schemas — the first post-import INSERT
11344                        // without an explicit id then violated NOT
11345                        // NULL. Lower it to the auto-increment
11346                        // marker instead.
11347                        let is_default_nextval =
11348                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11349                                && matches!(
11350                                    self.tokens.get(self.pos + 2),
11351                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11352                                );
11353                        if is_default_nextval {
11354                            let seq_name = self.scan_sequence_name_until_boundary();
11355                            return Ok(alloc::vec![
11356                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11357                                    column: col_name,
11358                                    seq_name,
11359                                }
11360                            ]);
11361                        }
11362                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11363                        self.advance(); // consume "set"
11364                        match self.peek().clone() {
11365                            Token::Default => {
11366                                self.advance();
11367                                let default_expr = self.parse_expr(0)?;
11368                                return Ok(alloc::vec![
11369                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11370                                        column: col_name,
11371                                        default_expr,
11372                                    }
11373                                ]);
11374                            }
11375                            Token::Not => {
11376                                self.advance();
11377                                if !matches!(self.peek(), Token::Null) {
11378                                    return Err(self.err(alloc::format!(
11379                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11380                                        self.peek()
11381                                    )));
11382                                }
11383                                self.advance();
11384                                return Ok(alloc::vec![
11385                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11386                                        column: col_name,
11387                                    }
11388                                ]);
11389                            }
11390                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11391                            // stored generated column's expression and
11392                            // recompute existing rows.
11393                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11394                                self.advance(); // EXPRESSION
11395                                if matches!(self.peek(), Token::As) {
11396                                    self.advance();
11397                                }
11398                                let expr = self.parse_expr(0)?;
11399                                return Ok(alloc::vec![
11400                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11401                                        column: col_name,
11402                                        expr,
11403                                    }
11404                                ]);
11405                            }
11406                            other => {
11407                                // Other SET subjects (STATISTICS,
11408                                // STORAGE, COMPRESSION, …) stay no-ops —
11409                                // storage hints with no SPG semantics.
11410                                let _ = other;
11411                                self.consume_until_statement_boundary();
11412                                return Ok(Vec::new());
11413                            }
11414                        }
11415                    }
11416                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11417                        self.advance(); // consume "drop"
11418                        return self.parse_alter_column_drop_tail(col_name);
11419                    }
11420                    Token::Drop => {
11421                        self.advance(); // consume Drop token
11422                        return self.parse_alter_column_drop_tail(col_name);
11423                    }
11424                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11425                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11426                        // GENERATED { ALWAYS | BY DEFAULT } AS
11427                        // IDENTITY ( … )`: pg_dump's spelling for
11428                        // identity columns. Same auto-increment
11429                        // lowering as the nextval default; the
11430                        // sequence options inside the parens are
11431                        // no-ops under SPG's max+1 semantics.
11432                        let is_generated = matches!(
11433                            self.tokens.get(self.pos + 1),
11434                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11435                        );
11436                        if !is_generated {
11437                            return Err(self.err(alloc::format!(
11438                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11439                                self.tokens.get(self.pos + 1)
11440                            )));
11441                        }
11442                        let seq_name = self.scan_sequence_name_until_boundary();
11443                        return Ok(alloc::vec![
11444                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11445                                column: col_name,
11446                                seq_name,
11447                            }
11448                        ]);
11449                    }
11450                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11451                    // column: floor the next allocated value at n (bare
11452                    // RESTART = restart from the start value, 1).
11453                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11454                        self.advance();
11455                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11456                        {
11457                            self.advance();
11458                            let neg = if matches!(self.peek(), Token::Minus) {
11459                                self.advance();
11460                                true
11461                            } else {
11462                                false
11463                            };
11464                            match self.advance() {
11465                                Token::Integer(v) => Some(if neg { -v } else { v }),
11466                                other => {
11467                                    return Err(self.err(alloc::format!(
11468                                        "expected integer after RESTART WITH, got {other:?}"
11469                                    )));
11470                                }
11471                            }
11472                        } else {
11473                            None
11474                        };
11475                        return Ok(alloc::vec![
11476                            crate::ast::AlterTableTarget::AlterColumnRestart {
11477                                column: col_name,
11478                                with,
11479                            }
11480                        ]);
11481                    }
11482                    other => {
11483                        return Err(self.err(alloc::format!(
11484                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11485                        )));
11486                    }
11487                }
11488                // v7.39 (round 713) — the type parser has consumed a
11489                // trailing `COLLATE <name>` since Phase 2.5, and
11490                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11491                // TYPE text COLLATE "C"` parsed clean and changed
11492                // nothing. Keep the clause; the engine re-collates.
11493                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11494                    self.parse_type_with_implied_flags()?;
11495                let collation = if coll_explicit {
11496                    coll_name.map(|n| (coll, n))
11497                } else {
11498                    None
11499                };
11500                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11501                {
11502                    self.advance();
11503                    Some(self.parse_expr(0)?)
11504                } else {
11505                    None
11506                };
11507                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11508                    column: col_name,
11509                    new_type,
11510                    using,
11511                    collation,
11512                }])
11513            }
11514            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11515            // PG also supports `RENAME TO new_table` for table-name
11516            // rename; that surface is deferred (pg_dump never emits
11517            // it). If the first post-RENAME ident is `TO`, the user
11518            // is asking for table rename — error with a clear
11519            // message rather than misparsing `TO` as a column name.
11520            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11521                self.advance();
11522                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11523                // table-name rename (mailrs round-10 A.5 — used
11524                // by migrate-042's `RENAME TO email_contacts`).
11525                // `TO` lexes as Token::To.
11526                if matches!(self.peek(), Token::To)
11527                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11528                {
11529                    self.advance();
11530                    let new = self.expect_ident_like()?;
11531                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11532                        new,
11533                    }]);
11534                }
11535                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11536                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11537                    self.advance();
11538                    let old = self.expect_ident_like()?;
11539                    if matches!(self.peek(), Token::To) {
11540                        self.advance();
11541                    } else {
11542                        self.expect_keyword_ident("to")?;
11543                    }
11544                    let new = self.expect_ident_like()?;
11545                    return Ok(alloc::vec![
11546                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11547                    ]);
11548                }
11549                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11550                    self.advance();
11551                }
11552                let old = self.expect_ident_like()?;
11553                // `TO` is a reserved keyword token; accept both
11554                // Token::To and Token::Ident("to") for consistency.
11555                if matches!(self.peek(), Token::To) {
11556                    self.advance();
11557                } else {
11558                    self.expect_keyword_ident("to")?;
11559                }
11560                let new = self.expect_ident_like()?;
11561                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11562                    old,
11563                    new,
11564                }])
11565            }
11566            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11567            // { ALL | <name> }`. pg_dump --disable-triggers wraps
11568            // every data block with these. Real disable semantics —
11569            // not no-op — because reload correctness assumes the
11570            // triggers don't fire (rows already carry their
11571            // computed values from prod).
11572            Token::Ident(s)
11573                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11574            {
11575                let enabled = s.eq_ignore_ascii_case("enable");
11576                self.advance();
11577                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11578                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11579                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11580                // pg_dump output) — anything else falls through to
11581                // the catch-all error below.
11582                // v7.22 (round-13 T3) — mysqldump wraps every data
11583                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11584                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11585                // maintains indexes incrementally — engine no-op.
11586                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11587                    self.advance();
11588                    return Ok(Vec::new());
11589                }
11590                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11591                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11592                // to gate triggers on session_replication_role; SPG
11593                // has no replica role, so the prefix is consumed and
11594                // treated identically to the plain ENABLE/DISABLE
11595                // TRIGGER form.
11596                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11597                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11598                {
11599                    self.advance();
11600                }
11601                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11602                    return Err(self.err(alloc::format!(
11603                        "expected TRIGGER after {}, got {:?}",
11604                        if enabled { "ENABLE" } else { "DISABLE" },
11605                        self.peek()
11606                    )));
11607                }
11608                self.advance();
11609                // `ALL` lexes as Token::All (reserved); also
11610                // accept Token::Ident("all") for symmetry.
11611                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11612                // TRIGGER selectors. USER (= all user triggers) is
11613                // semantically ALL here; REPLICA / ALWAYS gate on
11614                // session_replication_role which SPG doesn't track.
11615                // All map to TriggerSelector::All.
11616                let which = if matches!(self.peek(), Token::All)
11617                    || matches!(self.peek(), Token::Ident(s)
11618                        if s.eq_ignore_ascii_case("all")
11619                            || s.eq_ignore_ascii_case("user")
11620                            || s.eq_ignore_ascii_case("replica")
11621                            || s.eq_ignore_ascii_case("always"))
11622                {
11623                    self.advance();
11624                    crate::ast::TriggerSelector::All
11625                } else {
11626                    let name = self.expect_ident_like()?;
11627                    crate::ast::TriggerSelector::Named(name)
11628                };
11629                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11630                    which,
11631                    enabled,
11632                }])
11633            }
11634            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11635            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11636                self.advance();
11637                if !matches!(self.peek(), Token::Partition)
11638                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11639                        if s.eq_ignore_ascii_case("partition"))
11640                {
11641                    return Err(self.err(alloc::format!(
11642                        "expected PARTITION after ATTACH, got {:?}",
11643                        self.peek()
11644                    )));
11645                }
11646                self.advance();
11647                let child = self.expect_ident_like()?;
11648                let bounds = self.parse_partition_bounds_tail()?;
11649                Ok(alloc::vec![
11650                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11651                ])
11652            }
11653            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11654            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11655                self.advance();
11656                if !matches!(self.peek(), Token::Partition)
11657                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11658                        if s.eq_ignore_ascii_case("partition"))
11659                {
11660                    return Err(self.err(alloc::format!(
11661                        "expected PARTITION after DETACH, got {:?}",
11662                        self.peek()
11663                    )));
11664                }
11665                self.advance();
11666                let child = self.expect_ident_like()?;
11667                let mut concurrently = false;
11668                let mut finalize = false;
11669                loop {
11670                    match self.peek().clone() {
11671                        Token::Ident(s) | Token::QuotedIdent(s)
11672                            if s.eq_ignore_ascii_case("concurrently") =>
11673                        {
11674                            self.advance();
11675                            concurrently = true;
11676                        }
11677                        Token::Ident(s) | Token::QuotedIdent(s)
11678                            if s.eq_ignore_ascii_case("finalize") =>
11679                        {
11680                            self.advance();
11681                            finalize = true;
11682                        }
11683                        _ => break,
11684                    }
11685                }
11686                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11687                    child,
11688                    concurrently,
11689                    finalize,
11690                }])
11691            }
11692            other => Err(self.err(alloc::format!(
11693                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11694            ))),
11695        }
11696    }
11697
11698    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11699    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11700    /// TABLE … ATTACH PARTITION. Shares the same grammar as
11701    /// `parse_partition_of_tail`'s bounds branch.
11702    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11703    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11704    /// lowering each to the respective AlterTableTarget. Any
11705    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11706    /// no-op via consume_until_statement_boundary.
11707    fn parse_alter_column_drop_tail(
11708        &mut self,
11709        col_name: String,
11710    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11711        match self.peek().clone() {
11712            Token::Default => {
11713                self.advance();
11714                Ok(alloc::vec![
11715                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11716                ])
11717            }
11718            Token::Not => {
11719                self.advance();
11720                if !matches!(self.peek(), Token::Null) {
11721                    return Err(self.err(alloc::format!(
11722                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11723                        self.peek()
11724                    )));
11725                }
11726                self.advance();
11727                Ok(alloc::vec![
11728                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11729                ])
11730            }
11731            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11732            // generated column into a plain column.
11733            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11734                self.advance();
11735                // v7.39 (round 187, U10) — IF EXISTS was consumed but
11736                // dropped, so the engine still errored on a plain
11737                // column; PG's semantics are NOTICE + skip.
11738                let mut if_exists = false;
11739                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11740                    self.advance();
11741                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11742                        self.advance();
11743                        if_exists = true;
11744                    }
11745                }
11746                Ok(alloc::vec![
11747                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
11748                        column: col_name,
11749                        if_exists,
11750                    }
11751                ])
11752            }
11753            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11754            // identity column into a plain column.
11755            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11756                self.advance();
11757                let mut if_exists = false;
11758                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11759                    self.advance();
11760                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11761                        self.advance();
11762                        if_exists = true;
11763                    }
11764                }
11765                Ok(alloc::vec![
11766                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11767                        column: col_name,
11768                        if_exists,
11769                    }
11770                ])
11771            }
11772            _ => {
11773                self.consume_until_statement_boundary();
11774                Ok(Vec::new())
11775            }
11776        }
11777    }
11778
11779    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11780    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11781    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11782    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11783    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11784        let mut opts = crate::ast::CopyOptions::default();
11785        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11786            return Ok(opts);
11787        }
11788        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11789            self.advance();
11790        }
11791        if matches!(self.peek(), Token::LParen) {
11792            self.advance();
11793            loop {
11794                self.parse_one_copy_option(&mut opts)?;
11795                match self.peek() {
11796                    Token::Comma => {
11797                        self.advance();
11798                    }
11799                    Token::RParen => {
11800                        self.advance();
11801                        break;
11802                    }
11803                    other => {
11804                        return Err(self.err(alloc::format!(
11805                            "expected ',' or ')' in COPY options, got {other:?}"
11806                        )));
11807                    }
11808                }
11809            }
11810        } else {
11811            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11812                self.parse_one_copy_option(&mut opts)?;
11813            }
11814        }
11815        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11816            return Err(self.err(alloc::format!(
11817                "unexpected token after COPY options: {:?}",
11818                self.peek()
11819            )));
11820        }
11821        Ok(opts)
11822    }
11823
11824    fn parse_one_copy_option(
11825        &mut self,
11826        opts: &mut crate::ast::CopyOptions,
11827    ) -> Result<(), ParseError> {
11828        use crate::ast::CopyFormat;
11829        // The option keyword. NULL lexes as its own token; the rest are
11830        // bare identifiers.
11831        let kw = match self.advance() {
11832            Token::Null => alloc::string::String::from("NULL"),
11833            Token::Ident(s) => s.to_uppercase(),
11834            other => {
11835                return Err(self.err(alloc::format!(
11836                    "expected a COPY option keyword, got {other:?}"
11837                )));
11838            }
11839        };
11840        match kw.as_str() {
11841            "FORMAT" => {
11842                let fmt = self.expect_ident_like()?;
11843                match fmt.to_ascii_uppercase().as_str() {
11844                    "CSV" => opts.format = CopyFormat::Csv,
11845                    "TEXT" => opts.format = CopyFormat::Text,
11846                    other => {
11847                        return Err(self.err(alloc::format!(
11848                            "COPY format \"{}\" not recognized",
11849                            other.to_ascii_lowercase()
11850                        )));
11851                    }
11852                }
11853            }
11854            // Legacy bare format keywords.
11855            "CSV" => opts.format = CopyFormat::Csv,
11856            "TEXT" => opts.format = CopyFormat::Text,
11857            "HEADER" => {
11858                opts.header = match self.peek() {
11859                    Token::True => {
11860                        self.advance();
11861                        true
11862                    }
11863                    Token::False => {
11864                        self.advance();
11865                        false
11866                    }
11867                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11868                        self.advance();
11869                        true
11870                    }
11871                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11872                        self.advance();
11873                        false
11874                    }
11875                    // Bare HEADER (no boolean) means HEADER true.
11876                    _ => true,
11877                };
11878            }
11879            // r1066 (7.38 S5.1) — pgbench 14+ loads with
11880            // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
11881            // vacuum bookkeeping on a freshly created/truncated
11882            // table; SPG's per-statement visibility makes it a
11883            // faithful no-op, and rejecting it aborted `pgbench -i`
11884            // against the drop-in. Accept ON/OFF/bare, change nothing.
11885            "FREEZE" => match self.peek() {
11886                Token::True | Token::False => {
11887                    self.advance();
11888                }
11889                Token::Ident(s)
11890                    if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
11891                {
11892                    self.advance();
11893                }
11894                _ => {}
11895            },
11896            "DELIMITER" | "QUOTE" | "ESCAPE" => {
11897                let s = match self.advance() {
11898                    Token::String(s) => s,
11899                    other => {
11900                        return Err(self.err(alloc::format!(
11901                            "COPY {kw} expects a single-character string, got {other:?}"
11902                        )));
11903                    }
11904                };
11905                // v7.39 (round 247) — PG's wording (0A000), keyword in
11906                // lowercase: "COPY delimiter must be a single one-byte
11907                // character".
11908                let one_byte_err = || {
11909                    self.err(alloc::format!(
11910                        "COPY {} must be a single one-byte character",
11911                        kw.to_ascii_lowercase()
11912                    ))
11913                };
11914                let mut chars = s.chars();
11915                let c = chars.next().ok_or_else(one_byte_err)?;
11916                if chars.next().is_some() || c.len_utf8() != 1 {
11917                    return Err(one_byte_err());
11918                }
11919                match kw.as_str() {
11920                    "DELIMITER" => opts.delimiter = Some(c),
11921                    "QUOTE" => opts.quote = Some(c),
11922                    _ => opts.escape = Some(c),
11923                }
11924            }
11925            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11926            "FORCE_QUOTE" => {
11927                if matches!(self.peek(), Token::Star) {
11928                    self.advance();
11929                    opts.force_quote = Some(Vec::new());
11930                } else {
11931                    if !matches!(self.peek(), Token::LParen) {
11932                        return Err(self.err(alloc::format!(
11933                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11934                            self.peek()
11935                        )));
11936                    }
11937                    self.advance();
11938                    let mut cols = Vec::new();
11939                    loop {
11940                        cols.push(self.expect_ident_like()?);
11941                        match self.peek() {
11942                            Token::Comma => {
11943                                self.advance();
11944                            }
11945                            Token::RParen => {
11946                                self.advance();
11947                                break;
11948                            }
11949                            other => {
11950                                return Err(self.err(alloc::format!(
11951                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11952                                )));
11953                            }
11954                        }
11955                    }
11956                    opts.force_quote = Some(cols);
11957                }
11958            }
11959            "NULL" => {
11960                opts.null_str = Some(match self.advance() {
11961                    Token::String(s) => s,
11962                    other => {
11963                        return Err(self.err(alloc::format!(
11964                            "COPY NULL expects a quoted string, got {other:?}"
11965                        )));
11966                    }
11967                });
11968            }
11969            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11970            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11971            // FORCE_NULL too.
11972            "FORCE_NOT_NULL" | "FORCE_NULL" => {
11973                let cols = self.parse_copy_column_list(&kw)?;
11974                if kw == "FORCE_NOT_NULL" {
11975                    opts.force_not_null = Some(cols);
11976                } else {
11977                    opts.force_null = Some(cols);
11978                }
11979            }
11980            other => {
11981                // PG's wording, lowercased option name.
11982                return Err(self.err(alloc::format!(
11983                    "option \"{}\" not recognized",
11984                    other.to_ascii_lowercase()
11985                )));
11986            }
11987        }
11988        Ok(())
11989    }
11990
11991    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11992    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11993    /// is the `*` spelling.
11994    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11995        if matches!(self.peek(), Token::Star) {
11996            self.advance();
11997            return Ok(Vec::new());
11998        }
11999        if !matches!(self.peek(), Token::LParen) {
12000            return Err(self.err(alloc::format!(
12001                "expected '(' or '*' after {kw}, got {:?}",
12002                self.peek()
12003            )));
12004        }
12005        self.advance();
12006        let mut cols = Vec::new();
12007        loop {
12008            cols.push(self.expect_ident_like()?);
12009            match self.peek() {
12010                Token::Comma => {
12011                    self.advance();
12012                }
12013                Token::RParen => {
12014                    self.advance();
12015                    break;
12016                }
12017                other => {
12018                    return Err(self.err(alloc::format!(
12019                        "expected ',' or ')' in {kw} list, got {other:?}"
12020                    )));
12021                }
12022            }
12023        }
12024        Ok(cols)
12025    }
12026
12027    fn parse_partition_bounds_tail(
12028        &mut self,
12029    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12030        use crate::ast::PartitionOfBoundsAst;
12031        match self.peek() {
12032            Token::Default => {
12033                self.advance();
12034                Ok(PartitionOfBoundsAst::Default)
12035            }
12036            Token::For => {
12037                self.advance();
12038                if !matches!(self.peek(), Token::Values) {
12039                    return Err(
12040                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12041                    );
12042                }
12043                self.advance();
12044                let want_with = matches!(
12045                    self.peek(),
12046                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12047                );
12048                if want_with {
12049                    self.advance();
12050                    if !matches!(self.peek(), Token::LParen) {
12051                        return Err(self.err(format!(
12052                            "expected '(' after FOR VALUES WITH, got {:?}",
12053                            self.peek()
12054                        )));
12055                    }
12056                    self.advance();
12057                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12058                    loop {
12059                        let key = self.expect_ident_like()?;
12060                        let n = match self.peek().clone() {
12061                            Token::Integer(v) if u32::try_from(v).is_ok() => {
12062                                self.advance();
12063                                v as u32
12064                            }
12065                            other => {
12066                                return Err(self.err(format!(
12067                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12068                                )));
12069                            }
12070                        };
12071                        match key.to_ascii_uppercase().as_str() {
12072                            "MODULUS" => modulus = Some(n),
12073                            "REMAINDER" => remainder = Some(n),
12074                            other => {
12075                                return Err(self.err(format!(
12076                                    "FOR VALUES WITH: unknown key {other:?}; \
12077                                     expected MODULUS or REMAINDER"
12078                                )));
12079                            }
12080                        }
12081                        match self.peek() {
12082                            Token::Comma => {
12083                                self.advance();
12084                            }
12085                            Token::RParen => {
12086                                self.advance();
12087                                break;
12088                            }
12089                            other => {
12090                                return Err(self.err(format!(
12091                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12092                                )));
12093                            }
12094                        }
12095                    }
12096                    let modulus = modulus
12097                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12098                    let remainder = remainder.ok_or_else(|| {
12099                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12100                    })?;
12101                    if modulus == 0 {
12102                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12103                    }
12104                    if remainder >= modulus {
12105                        return Err(self.err(format!(
12106                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12107                        )));
12108                    }
12109                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12110                }
12111                match self.peek() {
12112                    Token::From => {
12113                        self.advance();
12114                        let lower = Box::new(self.parse_partition_bound_expr()?);
12115                        if !matches!(self.peek(), Token::To) {
12116                            return Err(self.err(format!(
12117                                "expected TO after FROM (...), got {:?}",
12118                                self.peek()
12119                            )));
12120                        }
12121                        self.advance();
12122                        let upper = Box::new(self.parse_partition_bound_expr()?);
12123                        Ok(PartitionOfBoundsAst::Range { lower, upper })
12124                    }
12125                    Token::In => {
12126                        self.advance();
12127                        if !matches!(self.peek(), Token::LParen) {
12128                            return Err(self.err(format!(
12129                                "expected '(' after FOR VALUES IN, got {:?}",
12130                                self.peek()
12131                            )));
12132                        }
12133                        self.advance();
12134                        let mut values = Vec::new();
12135                        loop {
12136                            values.push(self.parse_expr(0)?);
12137                            match self.peek() {
12138                                Token::Comma => {
12139                                    self.advance();
12140                                }
12141                                Token::RParen => {
12142                                    self.advance();
12143                                    break;
12144                                }
12145                                other => {
12146                                    return Err(self.err(format!(
12147                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12148                                    )));
12149                                }
12150                            }
12151                        }
12152                        if values.is_empty() {
12153                            return Err(
12154                                self.err("FOR VALUES IN requires at least one literal".to_string())
12155                            );
12156                        }
12157                        Ok(PartitionOfBoundsAst::List { values })
12158                    }
12159                    other => Err(self.err(format!(
12160                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12161                    ))),
12162                }
12163            }
12164            other => Err(self.err(format!(
12165                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12166            ))),
12167        }
12168    }
12169
12170    /// v7.16.2 — peek for `information_schema.<tbl>` /
12171    /// `pg_catalog.<tbl>` triples and, if matched, consume all
12172    /// three tokens + return a synthetic table name the engine's
12173    /// SELECT path recognises as a virtual view. Returns `None`
12174    /// when the head doesn't look like a meta-qualified name.
12175    /// Used by `parse_table_ref` to bypass the
12176    /// `expect_ident_like` schema-strip for these specific PG
12177    /// meta schemas (mailrs round-10 A.3).
12178    fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12179        // Extract the schema name. Must be a plain ident token.
12180        let schema = match self.tokens.get(self.pos) {
12181            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12182            _ => return None,
12183        };
12184        // Dot.
12185        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12186            return None;
12187        }
12188        // The table-side ident may lex as a reserved keyword
12189        // (e.g. `Token::Tables`). Tolerate the common ones via a
12190        // helper that reads the trailing token's underlying name.
12191        let tbl = match self.tokens.get(self.pos + 2)? {
12192            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12193            Token::Tables => "tables".to_string(),
12194            // Other PG meta table names that may collide with
12195            // reserved keywords land here as needed.
12196            _ => return None,
12197        };
12198        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12199        // names so the synthetic name doesn't double-prefix
12200        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12201        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12202            ("__spg_info_", tbl.to_ascii_lowercase())
12203        } else if schema.eq_ignore_ascii_case("pg_catalog") {
12204            // v7.39 (round 541) — only the catalogs SPG actually
12205            // synthesises are rewritten, which is what the BARE path
12206            // has always checked. Anything else keeps its own name and
12207            // takes the ordinary route: `pg_stat_activity` and friends
12208            // resolve through meta_view_result, and a name that is no
12209            // catalog at all gets PG's "relation does not exist"
12210            // instead of a message about a view SPG cannot materialise.
12211            let lowered = tbl.to_ascii_lowercase();
12212            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12213                self.advance(); // schema
12214                self.advance(); // dot
12215                self.advance(); // tbl
12216                return Some((lowered.clone(), lowered));
12217            }
12218            let bare = lowered
12219                .strip_prefix("pg_")
12220                .map(alloc::string::String::from)
12221                .unwrap_or(lowered);
12222            ("__spg_pg_", bare)
12223        } else if schema.eq_ignore_ascii_case("mysql") {
12224            // v7.17.0 Phase 3.P0-65 — MySQL system schema
12225            // (`mysql.user`, `mysql.db`). Same synthetic-name
12226            // shape as pg_catalog.
12227            ("__spg_mysql_", tbl.to_ascii_lowercase())
12228        } else {
12229            return None;
12230        };
12231        self.advance(); // schema
12232        self.advance(); // dot
12233        self.advance(); // tbl
12234        Some((
12235            alloc::format!("{prefix}{normalised}"),
12236            tbl.to_ascii_lowercase(),
12237        ))
12238    }
12239
12240    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12241    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12242    /// implicit front of every search_path, so a bare reference to a
12243    /// known catalog table always means the catalog table. Only the
12244    /// names the engine actually synthesises are recognised — any
12245    /// other `pg_*` ident stays a user table (mailrs embed round-12).
12246    fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12247        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12248        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12249        // `pg_catalog` at the front of every search_path. (pg_stat_activity
12250        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12251        // through the meta_view_result path instead, and already resolve
12252        // bare — they must NOT be listed here or the __spg_ rewrite would
12253        // mis-target them.)
12254        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12255        let name = match self.tokens.get(self.pos) {
12256            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12257            _ => return None,
12258        };
12259        // A following dot means this ident is a schema qualifier,
12260        // not a table name — let the qualified path handle it.
12261        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12262            return None;
12263        }
12264        if !PG_META_TABLES.contains(&name.as_str()) {
12265            return None;
12266        }
12267        self.advance();
12268        let bare = name.strip_prefix("pg_").unwrap_or(&name);
12269        Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12270    }
12271
12272    /// Consume a bare ident if its lowercase matches `kw`, else err.
12273    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12274    /// Peeks only; the caller advances.
12275    fn peek_keyword_ident(&self, kw: &str) -> bool {
12276        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12277    }
12278
12279    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12280        match self.advance() {
12281            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12282            other => Err(ParseError {
12283                message: format!("expected {kw:?}, got {other:?}"),
12284                token_pos: self.consumed_pos(),
12285            }),
12286        }
12287    }
12288
12289    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12290    /// literal (`'foo'`) — same shape used by CREATE USER for the
12291    /// username slot.
12292    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12293        match self.advance() {
12294            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12295            other => Err(ParseError {
12296                message: format!("expected identifier or string, got {other:?}"),
12297                token_pos: self.consumed_pos(),
12298            }),
12299        }
12300    }
12301
12302    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12303        match self.advance() {
12304            Token::String(s) => Ok(s),
12305            other => Err(ParseError {
12306                message: format!("expected quoted string, got {other:?}"),
12307                token_pos: self.consumed_pos(),
12308            }),
12309        }
12310    }
12311
12312    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12313        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12314        // subqueries recurse through here without passing
12315        // parse_expr; share the same nesting budget.
12316        self.enter_nested()?;
12317        let r = self.parse_select_stmt_inner();
12318        self.nest_depth -= 1;
12319        r
12320    }
12321
12322    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12323        // Caller dispatches on Token::Select; the inner helper handles
12324        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12325        // get a fresh bare-select parse and may not have their own ORDER
12326        // BY / LIMIT.
12327        let mut head = self.parse_bare_select()?;
12328        let into = self.pending_select_into.take();
12329        self.parse_setop_chain_into(&mut head)?;
12330        self.parse_select_tail_into(&mut head)?;
12331        // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12332        // `CREATE TABLE t AS SELECT …`, which is what a comment in
12333        // `ast.rs` has claimed since v7.38 and what only CTAS actually
12334        // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12335        // to the body, as it does in PostgreSQL.
12336        if let Some((name, temporary)) = into {
12337            return Ok(Statement::CreateMaterializedView(
12338                crate::ast::CreateMaterializedViewStatement {
12339                    temporary,
12340                    name,
12341                    if_not_exists: false,
12342                    columns: Vec::new(),
12343                    body: head,
12344                    with_data: true,
12345                    as_plain_table: true,
12346                },
12347            ));
12348        }
12349        Ok(Statement::Select(head))
12350    }
12351
12352    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12353    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12354    /// token), and INTERSECT [ALL] (a bare ident — it was never
12355    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12356    /// tighter than UNION / EXCEPT — the executor folds the chain
12357    /// left-to-right, which is already correct for LEADING
12358    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12359    /// pair nests into that previous peer, so A UNION B INTERSECT C
12360    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12361    /// groups.
12362    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12363        // A parenthesized group arrives with its own (already
12364        // regrouped) unions on `head`; only the pairs THIS chain
12365        // appends participate in the precedence regroup below —
12366        // nesting an outer INTERSECT into a group-internal peer
12367        // would dissolve the explicit grouping.
12368        let boundary = head.unions.len();
12369        loop {
12370            let base = match self.peek() {
12371                Token::Union => UnionKind::Distinct,
12372                Token::Except => UnionKind::Except,
12373                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12374                _ => break,
12375            };
12376            self.advance();
12377            let kind = if matches!(self.peek(), Token::All) {
12378                self.advance();
12379                match base {
12380                    UnionKind::Distinct => UnionKind::All,
12381                    UnionKind::Except => UnionKind::ExceptAll,
12382                    _ => UnionKind::IntersectAll,
12383                }
12384            } else {
12385                base
12386            };
12387            let peer = self.parse_bare_select()?;
12388            head.unions.push((kind, peer));
12389        }
12390        let mut pairs = core::mem::take(&mut head.unions);
12391        let tail = pairs.split_off(boundary);
12392        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12393        for (kind, peer) in tail {
12394            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12395            // An intersect nests into the previous element of THIS
12396            // chain only; with no new previous element it stays at
12397            // the outer level (the left fold applies it to the
12398            // whole head, group included).
12399            match (
12400                is_intersect,
12401                regrouped.len() > boundary,
12402                regrouped.last_mut(),
12403            ) {
12404                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12405                _ => regrouped.push((kind, peer)),
12406            }
12407        }
12408        head.unions = regrouped;
12409        Ok(())
12410    }
12411
12412    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12413    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12414    /// the top-level bare VALUES statement reuses it verbatim.
12415    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12416    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12417    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12418    /// where the grouping-set universe is still in scope.
12419    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12420        if !matches!(self.peek(), Token::Order) {
12421            return Ok(Vec::new());
12422        }
12423        self.advance();
12424        if !self.peek_is_by() {
12425            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12426        }
12427        self.advance();
12428        let mut keys = Vec::new();
12429        loop {
12430            // v7.39 (round 691) — save/restore, the discipline this parser
12431            // already uses around `pending_sample_preds`, so a subquery inside
12432            // a key neither inherits nor leaks the channel.
12433            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12434            let saved_coll = self.order_key_collation.take();
12435            let parsed = self.parse_expr(0);
12436            self.in_order_by_key = saved_flag;
12437            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12438            let expr = parsed?;
12439            let desc = if matches!(self.peek(), Token::Desc) {
12440                self.advance();
12441                true
12442            } else if matches!(self.peek(), Token::Asc) {
12443                self.advance();
12444                false
12445            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12446                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12447                // one ordering per type, so the btree comparison operators map
12448                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12449                // would need a custom operator class — honest error.
12450                self.advance();
12451                match self.advance() {
12452                    Token::Lt | Token::LtEq => false,
12453                    Token::Gt | Token::GtEq => true,
12454                    other => {
12455                        return Err(self.err(alloc::format!(
12456                            "ORDER BY USING supports the btree comparison \
12457                             operators (< <= > >=); got {other:?}"
12458                        )));
12459                    }
12460                }
12461            } else {
12462                false
12463            };
12464            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12465            let nulls_first = self.parse_optional_nulls_placement()?;
12466            keys.push(OrderBy {
12467                expr,
12468                desc,
12469                nulls_first,
12470                collation,
12471            });
12472            if matches!(self.peek(), Token::Comma) {
12473                self.advance();
12474            } else {
12475                break;
12476            }
12477        }
12478        Ok(keys)
12479    }
12480
12481    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12482        // v7.39 (round 135) — a grouping-set query may have already parsed +
12483        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12484        // no ORDER BY token is present, keep that pre-set order_by rather than
12485        // clobbering it with an empty list.
12486        let parsed_keys = self.parse_order_by_keys()?;
12487        head.order_by = if parsed_keys.is_empty() {
12488            core::mem::take(&mut head.order_by)
12489        } else {
12490            parsed_keys
12491        };
12492        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12493        // order. PG's grammar takes a limit clause and an offset clause
12494        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12495        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12496        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12497        // spelling died on `expected end of input, got Limit`.
12498        //
12499        // Each may appear at most once, and LIMIT and FETCH FIRST are
12500        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12501        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12502        // A second one is left unconsumed here, which the caller reports
12503        // as trailing input rather than silently taking the last.
12504        let mut saw_limit = false;
12505        let mut saw_offset = false;
12506        loop {
12507            if !saw_limit && matches!(self.peek(), Token::Limit) {
12508                self.advance();
12509                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12510                // PG synonyms for "no limit". Treat both as None
12511                // (no head.limit set) so the engine's existing
12512                // unlimited-result path takes over. Reject was the
12513                // pre-5.1 behaviour and broke pg_dump-flavoured
12514                // tooling that occasionally emits LIMIT NULL.
12515                if self.consume_limit_unbounded_sentinel() {
12516                    head.limit = None;
12517                } else {
12518                    let first = self.parse_limit_expr("LIMIT")?;
12519                    // MySQL `LIMIT offset, count` — the first number is
12520                    // the offset when a comma follows.
12521                    if matches!(self.peek(), Token::Comma) {
12522                        self.advance();
12523                        let count = self.parse_limit_expr("LIMIT")?;
12524                        head.offset = Some(first);
12525                        saw_offset = true;
12526                        head.limit = Some(count);
12527                    } else {
12528                        head.limit = Some(first);
12529                    }
12530                }
12531                saw_limit = true;
12532                continue;
12533            }
12534            if !saw_offset && matches!(self.peek(), Token::Offset) {
12535                self.advance();
12536                // PG also accepts an optional `ROW` / `ROWS` trailer
12537                // after the offset value (`OFFSET 10 ROWS`). The
12538                // FETCH-FIRST branch below relies on the same.
12539                let off = self.parse_limit_expr("OFFSET")?;
12540                self.consume_optional_rows_keyword();
12541                head.offset = Some(off);
12542                saw_offset = true;
12543                continue;
12544            }
12545            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12546            // the SQL-standard alias for LIMIT. PG accepts both
12547            // spellings interchangeably; pg_dump emits FETCH FIRST in
12548            // newer versions. We map it onto `head.limit` so the
12549            // engine path is unified.
12550            if !saw_limit
12551                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12552                    if s.eq_ignore_ascii_case("fetch"))
12553            {
12554                self.advance(); // FETCH
12555                // `FIRST` or `NEXT` (both legal per SQL standard).
12556                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12557                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12558                {
12559                    self.advance();
12560                }
12561                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12562                // implicit 1 — but we always consume one if present).
12563                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12564                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12565                {
12566                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12567                    crate::ast::LimitExpr::Literal(1)
12568                } else {
12569                    self.parse_limit_expr("FETCH FIRST")?
12570                };
12571                // Eat `ROW` / `ROWS` if not already consumed above.
12572                self.consume_optional_rows_keyword();
12573                // Optional `ONLY` (the spec form) — or the SQL:2008
12574                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12575                // now honours WITH TIES by extending past the LIMIT
12576                // truncation point through every row that shares the
12577                // last-kept row's ORDER BY key.
12578                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12579                    if s.eq_ignore_ascii_case("only"))
12580                {
12581                    self.advance();
12582                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12583                    if s.eq_ignore_ascii_case("with"))
12584                {
12585                    self.advance(); // WITH
12586                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12587                        if s.eq_ignore_ascii_case("ties"))
12588                    {
12589                        self.advance();
12590                        head.limit_with_ties = true;
12591                    }
12592                }
12593                head.limit = Some(count);
12594                saw_limit = true;
12595                continue;
12596            }
12597            break;
12598        }
12599        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12600        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12601        //       [ OF table_name [, …] ]
12602        //       [ NOWAIT | SKIP LOCKED ]
12603        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12604        // FOR SHARE OF t2`). SPG is a single-writer engine — every
12605        // SELECT already returns a consistent snapshot — so these
12606        // are accept-and-discard: the parser absorbs them so
12607        // mailrs / Rails / Django code paths that emit `SELECT
12608        // … FOR UPDATE` for advisory pessimistic locking load
12609        // without a parser error. The on-disk locking model is
12610        // unchanged; callers that rely on FOR UPDATE for read-
12611        // through-write ordering still get the right answer
12612        // because SPG serialises writes anyway.
12613        head.locking = self
12614            .consume_optional_for_lock_clauses()
12615            .map(alloc::boxed::Box::new);
12616        Ok(())
12617    }
12618
12619    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12620    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12621    /// LOCKED ]` trailers. Each clause is fully accepted and
12622    /// discarded — SPG's single-writer model already satisfies the
12623    /// callers' implicit ordering requirement. Stops at the first
12624    /// token that isn't `FOR`.
12625    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12626        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12627        // not discarded. PG keeps the strongest of several clauses; the
12628        // policy of the last one wins, which is what this loop records.
12629        let mut seen: Option<crate::ast::LockingClause> = None;
12630        while matches!(self.peek(), Token::For) {
12631            // v7.37.14 (A2.5-stub) — record that this query asked
12632            // for a row lock the parser is about to silently
12633            // discard. Operators surface the count via
12634            // `spg_sql::silent_for_update_count()` so they can
12635            // gauge how much of the workload depends on advisory
12636            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12637            // before v7.37.15's per-row tuple locking lands.
12638            crate::record_silent_for_update_clause();
12639            self.advance(); // FOR
12640            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12641            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12642            let mut no_key = false;
12643            let mut key = false;
12644            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12645                if s.eq_ignore_ascii_case("no"))
12646            {
12647                self.advance(); // NO
12648                no_key = true;
12649                // The next ident should be KEY but be generous;
12650                // anything followed by UPDATE/SHARE is accepted.
12651                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12652                    if s.eq_ignore_ascii_case("key"))
12653                {
12654                    self.advance(); // KEY
12655                }
12656            }
12657            // `KEY` prefix (PG `FOR KEY SHARE`).
12658            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12659                if s.eq_ignore_ascii_case("key"))
12660            {
12661                self.advance(); // KEY
12662                key = true;
12663            }
12664            // Lock-strength keyword: UPDATE / SHARE. Required, but
12665            // we're lenient — an unexpected token here just bails
12666            // (we already consumed FOR; caller's downstream
12667            // dispatch will error if anything actually depends on
12668            // the trailing tokens).
12669            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12670                if s.eq_ignore_ascii_case("update"));
12671            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12672                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12673            {
12674                self.advance();
12675                use crate::ast::LockStrength as LS;
12676                let strength = match (is_update, no_key, key) {
12677                    (true, true, _) => LS::NoKeyUpdate,
12678                    (true, _, _) => LS::Update,
12679                    (false, _, true) => LS::KeyShare,
12680                    (false, _, _) => LS::Share,
12681                };
12682                seen = Some(crate::ast::LockingClause {
12683                    strength,
12684                    of_tables: alloc::vec::Vec::new(),
12685                    policy: crate::ast::LockWait::Wait,
12686                });
12687            } else {
12688                // FOR by itself (or `FOR KEY` with nothing after) —
12689                // give up on the lock-clause path. We've already
12690                // advanced past FOR; further attempts to parse
12691                // here would clobber state.
12692                return seen;
12693            }
12694            // Optional `OF tbl[, tbl …]`. mailrs emits this when
12695            // joining and locking only a subset of tables.
12696            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12697                if s.eq_ignore_ascii_case("of"))
12698            {
12699                self.advance(); // OF
12700                #[allow(clippy::while_let_loop)]
12701                loop {
12702                    match self.peek() {
12703                        Token::Ident(_) | Token::QuotedIdent(_) => {
12704                            // v7.39 (round 294) — the name is CAPTURED now: PG
12705                            // validates it against the FROM clause, and an
12706                            // uncaptured list silently means "lock everything".
12707                            let mut nm = match self.advance() {
12708                                Token::Ident(n) | Token::QuotedIdent(n) => n,
12709                                _ => alloc::string::String::new(),
12710                            };
12711                            // Optional schema-qualified `schema.table`.
12712                            if matches!(self.peek(), Token::Dot) {
12713                                self.advance();
12714                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12715                                {
12716                                    self.advance();
12717                                    nm = n;
12718                                }
12719                            }
12720                            if let Some(c) = seen.as_mut() {
12721                                c.of_tables.push(nm);
12722                            }
12723                        }
12724                        _ => break,
12725                    }
12726                    if matches!(self.peek(), Token::Comma) {
12727                        self.advance();
12728                    } else {
12729                        break;
12730                    }
12731                }
12732            }
12733            // Optional `NOWAIT` | `SKIP LOCKED`.
12734            match self.peek().clone() {
12735                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12736                    self.advance();
12737                    if let Some(c) = seen.as_mut() {
12738                        c.policy = crate::ast::LockWait::NoWait;
12739                    }
12740                }
12741                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12742                    self.advance(); // SKIP
12743                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12744                        if s.eq_ignore_ascii_case("locked"))
12745                    {
12746                        self.advance(); // LOCKED
12747                        if let Some(c) = seen.as_mut() {
12748                            c.policy = crate::ast::LockWait::SkipLocked;
12749                        }
12750                    }
12751                }
12752                _ => {}
12753            }
12754            // Loop: PG allows multiple FOR clauses chained.
12755        }
12756        seen
12757    }
12758
12759    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12760    /// Bind value gets resolved during prepared-statement Execute;
12761    /// the Pratt expression parser would over-accept here (e.g.
12762    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12763    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12764    /// sentinel tokens (PG synonyms for "no limit"). Returns true
12765    /// when one was consumed; caller skips the regular
12766    /// limit-value parse and leaves `head.limit` at None.
12767    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12768        if matches!(self.peek(), Token::Null) {
12769            self.advance();
12770            return true;
12771        }
12772        if matches!(self.peek(), Token::All) {
12773            self.advance();
12774            return true;
12775        }
12776        false
12777    }
12778
12779    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12780    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12781    /// SQL-standard shape. No-op when missing.
12782    fn consume_optional_rows_keyword(&mut self) {
12783        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12784            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12785        {
12786            self.advance();
12787        }
12788    }
12789
12790    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12791    ///
12792    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12793    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12794    /// constant, which is why that spelling keeps the token path below.
12795    ///
12796    /// Constants are folded here rather than carried into the tree: the
12797    /// 15+ execution paths that read the row count go through
12798    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12799    /// means "no limit". A clause the engine could not resolve would
12800    /// therefore return the WHOLE table instead of failing. Folding at
12801    /// parse time keeps that impossible; a non-constant clause is still
12802    /// a clean error (recorded residual — closing it wants a resolution
12803    /// pre-pass on the simple-query path, where `substitute_placeholders`
12804    /// does not run).
12805    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12806        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12807        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12808        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12809        // ONLY` both work (its grammar takes a c_expr). Both measured
12810        // against PG 18.4 in round 305.
12811        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12812            return self.parse_limit_constant(label);
12813        }
12814        // One pass, no rewind: `advance()` takes each token by
12815        // `mem::replace`, so a consumed token reads back as Eof and this
12816        // parser cannot backtrack. Everything — bare literal included —
12817        // is therefore folded from the parsed expression rather than
12818        // re-read from the token stream.
12819        let start = self.pos;
12820        let e = self.parse_expr(0)?;
12821        if let crate::ast::Expr::Placeholder(n) = e {
12822            return Ok(crate::ast::LimitExpr::Placeholder(n));
12823        }
12824        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12825        match fold_limit_constant(&e) {
12826            Some(Ok(v)) if v < 0 => Err(ParseError {
12827                message: alloc::format!("{neg_label} must not be negative"),
12828                token_pos: start,
12829            }),
12830            Some(Ok(v)) => u32::try_from(v)
12831                .map(crate::ast::LimitExpr::Literal)
12832                .map_err(|_| ParseError {
12833                    message: alloc::format!("{label} value too large: {v}"),
12834                    token_pos: start,
12835                }),
12836            Some(Err(message)) => Err(ParseError {
12837                message: message.replace("{L}", neg_label),
12838                token_pos: start,
12839            }),
12840            // v7.39 (round 305, V23) — not foldable at parse time
12841            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12842            // expression; the engine evaluates it once before dispatch.
12843            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12844        }
12845    }
12846
12847    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12848        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12849        // coercion rules, not just an integer token: a NUMERIC rounds half
12850        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12851        // refused with PG's wording ("LIMIT must not be negative", 2201W /
12852        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12853        // content, failing as an input-syntax error on the value. General
12854        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12855        // they need an Expr-carrying LimitExpr variant.
12856        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12857        let err_at = |message: alloc::string::String, pos: usize| ParseError {
12858            message,
12859            token_pos: pos,
12860        };
12861        match self.advance() {
12862            Token::Integer(n) if n >= 0 => u32::try_from(n)
12863                .map(crate::ast::LimitExpr::Literal)
12864                .map_err(|_| ParseError {
12865                    message: alloc::format!("{label} value too large: {n}"),
12866                    token_pos: self.consumed_pos(),
12867                }),
12868            Token::Integer(_) => Err(err_at(
12869                alloc::format!("{neg_label} must not be negative"),
12870                self.pos.saturating_sub(1),
12871            )),
12872            Token::Numeric(t) => {
12873                let pos = self.pos.saturating_sub(1);
12874                let v: f64 = t.parse().map_err(|_| {
12875                    err_at(
12876                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12877                        pos,
12878                    )
12879                })?;
12880                if v < 0.0 {
12881                    return Err(err_at(
12882                        alloc::format!("{neg_label} must not be negative"),
12883                        pos,
12884                    ));
12885                }
12886                // Round half away from zero — PG's numeric→bigint cast.
12887                // (no_std: no f64::round; v is non-negative, so truncating
12888                // v + 0.5 is the same thing.)
12889                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12890                let rounded = (v + 0.5) as u64;
12891                u32::try_from(rounded)
12892                    .map(crate::ast::LimitExpr::Literal)
12893                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12894            }
12895            Token::Minus => {
12896                let pos = self.pos.saturating_sub(1);
12897                match self.peek() {
12898                    Token::Integer(_) | Token::Numeric(_) => {
12899                        self.advance();
12900                        Err(err_at(
12901                            alloc::format!("{neg_label} must not be negative"),
12902                            pos,
12903                        ))
12904                    }
12905                    other => Err(err_at(
12906                        alloc::format!(
12907                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12908                        ),
12909                        pos,
12910                    )),
12911                }
12912            }
12913            Token::String(t) => {
12914                let pos = self.pos.saturating_sub(1);
12915                match t.trim().parse::<i64>() {
12916                    Ok(n) if n < 0 => Err(err_at(
12917                        alloc::format!("{neg_label} must not be negative"),
12918                        pos,
12919                    )),
12920                    Ok(n) => u32::try_from(n)
12921                        .map(crate::ast::LimitExpr::Literal)
12922                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12923                    Err(_) => Err(err_at(
12924                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12925                        pos,
12926                    )),
12927                }
12928            }
12929            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12930            other => Err(ParseError {
12931                message: alloc::format!(
12932                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12933                ),
12934                token_pos: self.consumed_pos(),
12935            }),
12936        }
12937    }
12938
12939    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12940    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12941    /// `unions` empty and `order_by` / `limit` `None`; the top-level
12942    /// `parse_select_stmt` is responsible for filling those in.
12943    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12944    /// call in the expression tree to the per-set integer bitmask
12945    /// (PG semantics: one bit per argument, MSB first; 1 = the key
12946    /// is dropped in this grouping set). Runs during the ROLLUP /
12947    /// CUBE / GROUPING SETS expansion, where the set is known.
12948    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12949    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12950    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12951        if let Expr::FunctionCall { name, .. } = expr
12952            && name.eq_ignore_ascii_case("grouping")
12953        {
12954            if !out.iter().any(|e| e == expr) {
12955                out.push(expr.clone());
12956            }
12957            return;
12958        }
12959        match expr {
12960            Expr::Binary { lhs, rhs, .. } => {
12961                Self::collect_grouping_calls(lhs, out);
12962                Self::collect_grouping_calls(rhs, out);
12963            }
12964            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12965                Self::collect_grouping_calls(expr, out)
12966            }
12967            Expr::FunctionCall { args, .. } => {
12968                for a in args {
12969                    Self::collect_grouping_calls(a, out);
12970                }
12971            }
12972            Expr::Case {
12973                operand,
12974                branches,
12975                else_branch,
12976            } => {
12977                if let Some(o) = operand {
12978                    Self::collect_grouping_calls(o, out);
12979                }
12980                for (c, v) in branches {
12981                    Self::collect_grouping_calls(c, out);
12982                    Self::collect_grouping_calls(v, out);
12983                }
12984                if let Some(x) = else_branch {
12985                    Self::collect_grouping_calls(x, out);
12986                }
12987            }
12988            _ => {}
12989        }
12990    }
12991
12992    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12993    /// `grp_exprs[k]` with a reference to the synthetic ordering column
12994    /// `__grp_ord_k` (injected per grouping-set branch).
12995    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12996        if let Expr::FunctionCall { name, .. } = expr
12997            && name.eq_ignore_ascii_case("grouping")
12998        {
12999            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13000                *expr = Expr::Column(crate::ast::ColumnName {
13001                    qualifier: None,
13002                    name: alloc::format!("__grp_ord_{k}"),
13003                });
13004            }
13005            return;
13006        }
13007        match expr {
13008            Expr::Binary { lhs, rhs, .. } => {
13009                Self::rewrite_grouping_to_col(lhs, grp_exprs);
13010                Self::rewrite_grouping_to_col(rhs, grp_exprs);
13011            }
13012            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13013                Self::rewrite_grouping_to_col(expr, grp_exprs)
13014            }
13015            Expr::FunctionCall { args, .. } => {
13016                for a in args {
13017                    Self::rewrite_grouping_to_col(a, grp_exprs);
13018                }
13019            }
13020            Expr::Case {
13021                operand,
13022                branches,
13023                else_branch,
13024            } => {
13025                if let Some(o) = operand {
13026                    Self::rewrite_grouping_to_col(o, grp_exprs);
13027                }
13028                for (c, v) in branches {
13029                    Self::rewrite_grouping_to_col(c, grp_exprs);
13030                    Self::rewrite_grouping_to_col(v, grp_exprs);
13031                }
13032                if let Some(x) = else_branch {
13033                    Self::rewrite_grouping_to_col(x, grp_exprs);
13034                }
13035            }
13036            _ => {}
13037        }
13038    }
13039
13040    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13041    /// as the list of key sets it contributes. A bare expression is one
13042    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13043    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13044    /// the concatenation of its items' sets, where an item is itself an
13045    /// element, a parenthesized key list, or the empty set `()`. A
13046    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13047    /// move together.
13048    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13049        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13050        // ROLLUP ( … ) / CUBE ( … )
13051        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13052            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13053        {
13054            let is_cube = is_kw(self.peek(), "cube");
13055            self.advance(); // ROLLUP / CUBE
13056            self.advance(); // (
13057            let mut units: Vec<Vec<Expr>> = Vec::new();
13058            loop {
13059                if matches!(self.peek(), Token::LParen) {
13060                    // Composite unit: (a, b) rolls up as one.
13061                    self.advance();
13062                    let mut unit = Vec::new();
13063                    if !matches!(self.peek(), Token::RParen) {
13064                        loop {
13065                            unit.push(self.parse_expr(0)?);
13066                            match self.peek() {
13067                                Token::Comma => {
13068                                    self.advance();
13069                                }
13070                                Token::RParen => break,
13071                                other => {
13072                                    return Err(self.err(format!(
13073                                        "expected ',' or ')' in grouping unit, got {other:?}"
13074                                    )));
13075                                }
13076                            }
13077                        }
13078                    }
13079                    self.advance(); // )
13080                    units.push(unit);
13081                } else {
13082                    units.push(alloc::vec![self.parse_expr(0)?]);
13083                }
13084                match self.peek() {
13085                    Token::Comma => {
13086                        self.advance();
13087                    }
13088                    Token::RParen => break,
13089                    other => {
13090                        return Err(self.err(format!(
13091                            "expected ',' or ')' in grouping list, got {other:?}"
13092                        )));
13093                    }
13094                }
13095            }
13096            self.advance(); // )
13097            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13098                units
13099                    .iter()
13100                    .zip(unit_sel.iter())
13101                    .filter(|(_, keep)| **keep)
13102                    .flat_map(|(u, _)| u.iter().cloned())
13103                    .collect()
13104            };
13105            let n = units.len();
13106            if is_cube {
13107                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13108                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13109                    .collect();
13110                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13111                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13112            }
13113            return Ok((0..=n)
13114                .rev()
13115                .map(|keep| {
13116                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13117                    flatten(&sel)
13118                })
13119                .collect());
13120        }
13121        // GROUPING SETS ( item [, item]* )
13122        if is_kw(self.peek(), "grouping")
13123            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13124        {
13125            self.advance(); // GROUPING
13126            self.advance(); // SETS
13127            if !matches!(self.peek(), Token::LParen) {
13128                return Err(self.err(format!(
13129                    "expected '(' after GROUPING SETS, got {:?}",
13130                    self.peek()
13131                )));
13132            }
13133            self.advance(); // outer (
13134            let mut sets: Vec<Vec<Expr>> = Vec::new();
13135            loop {
13136                if matches!(self.peek(), Token::LParen) {
13137                    // A parenthesized key list (or the empty set).
13138                    self.advance();
13139                    let mut set = Vec::new();
13140                    if !matches!(self.peek(), Token::RParen) {
13141                        loop {
13142                            set.push(self.parse_expr(0)?);
13143                            match self.peek() {
13144                                Token::Comma => {
13145                                    self.advance();
13146                                }
13147                                Token::RParen => break,
13148                                other => {
13149                                    return Err(self.err(format!(
13150                                        "expected ',' or ')' in grouping set, got {other:?}"
13151                                    )));
13152                                }
13153                            }
13154                        }
13155                    }
13156                    self.advance(); // )
13157                    sets.push(set);
13158                } else {
13159                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13160                    // bare expression.
13161                    sets.extend(self.parse_grouping_element()?);
13162                }
13163                match self.peek() {
13164                    Token::Comma => {
13165                        self.advance();
13166                    }
13167                    Token::RParen => break,
13168                    other => {
13169                        return Err(self.err(format!(
13170                            "expected ',' or ')' after a grouping set, got {other:?}"
13171                        )));
13172                    }
13173                }
13174            }
13175            self.advance(); // outer )
13176            return Ok(sets);
13177        }
13178        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13179    }
13180
13181    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13182        // v7.38 (read01) — a reference to a key that is dropped in this grouping
13183        // set evaluates to NULL, at any depth. Previously only a *top-level*
13184        // select item equal to a dropped key was nullified, so a key nested in
13185        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13186        // column and failed to resolve against the set's synthetic schema.
13187        if dropped.iter().any(|d| d == expr) {
13188            *expr = Expr::Literal(Literal::Null);
13189            return;
13190        }
13191        if let Expr::FunctionCall { name, args } = expr
13192            && name.eq_ignore_ascii_case("grouping")
13193        {
13194            let mut mask: i64 = 0;
13195            for a in args.iter() {
13196                mask <<= 1;
13197                if dropped.iter().any(|d| d == a) {
13198                    mask |= 1;
13199                }
13200            }
13201            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13202            // literal: a bare integer in a select item is indistinguishable
13203            // from a positional reference once `ORDER BY 1` substitutes the
13204            // item back in, and the round-232 position check then read the
13205            // mask value as an out-of-range position. The cast changes
13206            // nothing semantically (grouping() is integer).
13207            *expr = Expr::Cast {
13208                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13209                target: crate::ast::CastTarget::Int,
13210            };
13211            return;
13212        }
13213        // Generic recursion over the common expression shapes the
13214        // SELECT list uses; anything without child expressions is
13215        // left alone.
13216        match expr {
13217            Expr::FunctionCall { args, .. } => {
13218                for a in args {
13219                    Self::substitute_grouping_calls(a, dropped);
13220                }
13221            }
13222            Expr::Binary { lhs, rhs, .. } => {
13223                Self::substitute_grouping_calls(lhs, dropped);
13224                Self::substitute_grouping_calls(rhs, dropped);
13225            }
13226            Expr::Unary { expr: inner, .. } => {
13227                Self::substitute_grouping_calls(inner, dropped);
13228            }
13229            Expr::Cast { expr: inner, .. } => {
13230                Self::substitute_grouping_calls(inner, dropped);
13231            }
13232            Expr::Case {
13233                operand,
13234                branches,
13235                else_branch,
13236            } => {
13237                if let Some(op) = operand {
13238                    Self::substitute_grouping_calls(op, dropped);
13239                }
13240                for (w, t) in branches {
13241                    Self::substitute_grouping_calls(w, dropped);
13242                    Self::substitute_grouping_calls(t, dropped);
13243                }
13244                if let Some(e) = else_branch {
13245                    Self::substitute_grouping_calls(e, dropped);
13246                }
13247            }
13248            // v7.38 (read01) — recurse into the remaining child-bearing shapes
13249            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13250            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13251            // …` is the canonical rollup-total label idiom).
13252            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13253            Expr::Like { expr, pattern, .. } => {
13254                Self::substitute_grouping_calls(expr, dropped);
13255                Self::substitute_grouping_calls(pattern, dropped);
13256            }
13257            Expr::InList { expr, list, .. } => {
13258                Self::substitute_grouping_calls(expr, dropped);
13259                for item in list {
13260                    Self::substitute_grouping_calls(item, dropped);
13261                }
13262            }
13263            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13264            Expr::Array(items) => {
13265                for item in items {
13266                    Self::substitute_grouping_calls(item, dropped);
13267                }
13268            }
13269            Expr::ArraySubscript { target, index } => {
13270                Self::substitute_grouping_calls(target, dropped);
13271                Self::substitute_grouping_calls(index, dropped);
13272            }
13273            Expr::ArraySlice { target, lo, hi } => {
13274                Self::substitute_grouping_calls(target, dropped);
13275                if let Some(lo) = lo {
13276                    Self::substitute_grouping_calls(lo, dropped);
13277                }
13278                if let Some(hi) = hi {
13279                    Self::substitute_grouping_calls(hi, dropped);
13280                }
13281            }
13282            Expr::AnyAll { expr, array, .. } => {
13283                Self::substitute_grouping_calls(expr, dropped);
13284                Self::substitute_grouping_calls(array, dropped);
13285            }
13286            _ => {}
13287        }
13288    }
13289
13290    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13291        // v7.37.17 (17.6 siblings) — parenthesized set-operation
13292        // group: `( <select chain> )` usable anywhere a query block
13293        // is (head or peer of an outer chain). The group's own
13294        // unions ride the returned SelectStatement; the executor's
13295        // nested-peer recursion runs them.
13296        if matches!(self.peek(), Token::LParen)
13297            && matches!(
13298                self.tokens.get(self.pos + 1),
13299                Some(Token::Select | Token::LParen | Token::Values)
13300            )
13301        {
13302            self.advance(); // (
13303            self.enter_nested()?;
13304            // v7.37 D.20 — a group whose head is a VALUES list:
13305            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13306            // otherwise recurse into a nested SELECT/group head.
13307            let mut head = (if matches!(self.peek(), Token::Values) {
13308                self.advance(); // VALUES
13309                self.parse_values_rows_body()
13310            } else {
13311                self.parse_bare_select()
13312            })
13313            .and_then(|mut h| {
13314                self.parse_setop_chain_into(&mut h)?;
13315                Ok(h)
13316            });
13317            self.nest_depth -= 1;
13318            let mut head = match &mut head {
13319                Ok(h) => core::mem::take(h),
13320                Err(_) => return head,
13321            };
13322            // v7.37.17 (17.6 siblings) — group-internal tail:
13323            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13324            // group head, then wrap the group as a derived table
13325            // (SELECT * FROM (group)) so the outer chain / outer
13326            // tail can't clobber the group's own ordering or limit.
13327            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13328                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13329                    if s.eq_ignore_ascii_case("fetch"));
13330            if has_tail {
13331                self.parse_select_tail_into(&mut head)?;
13332                head = SelectStatement {
13333                    locking: None,
13334                    ctes: Vec::new(),
13335                    distinct: false,
13336                    distinct_on: Vec::new(),
13337                    items: alloc::vec![SelectItem::Wildcard],
13338                    from: Some(FromClause {
13339                        primary: TableRef {
13340                            name: "subquery".to_string(),
13341                            alias: None,
13342                            only: false,
13343                            as_of_segment: None,
13344                            unnest_expr: None,
13345                            unnest_column_aliases: Vec::new(),
13346                            with_ordinality: false,
13347                            generate_series_args: None,
13348                            lateral_subquery: Some(Box::new(head)),
13349                            jsonb_each_text_arg: None,
13350                            table_fn_call: None,
13351                            rows_from: None,
13352                            json_table: None,
13353                            scalar_fn_item: false,
13354                        },
13355                        joins: Vec::new(),
13356                    }),
13357                    where_: None,
13358                    group_by: None,
13359                    group_by_all: false,
13360                    having: None,
13361                    unions: Vec::new(),
13362                    order_by: Vec::new(),
13363                    limit: None,
13364                    offset: None,
13365                    limit_with_ties: false,
13366                    window_check_exprs: Vec::new(),
13367                };
13368            }
13369            if !matches!(self.peek(), Token::RParen) {
13370                return Err(self.err(format!(
13371                    "expected ')' after parenthesized query group, got {:?}",
13372                    self.peek()
13373                )));
13374            }
13375            self.advance();
13376            return Ok(head);
13377        }
13378        // `TABLE name` shorthand as a query block — valid anywhere
13379        // a SELECT head is (set-op peers included).
13380        if matches!(self.peek(), Token::Table)
13381            && matches!(
13382                self.tokens.get(self.pos + 1),
13383                Some(Token::Ident(_) | Token::QuotedIdent(_))
13384            )
13385        {
13386            return self.parse_table_shorthand();
13387        }
13388        if !matches!(self.peek(), Token::Select) {
13389            return Err(self.err(format!(
13390                "expected SELECT to start a query block, got {:?}",
13391                self.peek()
13392            )));
13393        }
13394        self.advance();
13395        let distinct = if matches!(self.peek(), Token::Distinct) {
13396            self.advance();
13397            true
13398        } else {
13399            false
13400        };
13401        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13402        // keep the first row (per ORDER BY) of each group the
13403        // expressions define. Django's .distinct('field') shape.
13404        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13405            self.advance(); // ON
13406            if !matches!(self.peek(), Token::LParen) {
13407                return Err(self.err(format!(
13408                    "expected '(' after DISTINCT ON, got {:?}",
13409                    self.peek()
13410                )));
13411            }
13412            self.advance();
13413            let mut exprs = Vec::new();
13414            loop {
13415                exprs.push(self.parse_expr(0)?);
13416                match self.peek() {
13417                    Token::Comma => {
13418                        self.advance();
13419                    }
13420                    Token::RParen => break,
13421                    other => {
13422                        return Err(self.err(format!(
13423                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13424                        )));
13425                    }
13426                }
13427            }
13428            self.advance(); // )
13429            exprs
13430        } else {
13431            Vec::new()
13432        };
13433        let mut items = self.parse_select_list()?;
13434        // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13435        // of CTAS. It sits exactly here in PG's grammar, right after the
13436        // target list.
13437        //
13438        // A comment in `ast.rs` has said since v7.38 that CTAS and
13439        // `SELECT INTO` lower to the same node. Only CTAS ever did:
13440        // `SELECT i INTO t FROM src` answered `syntax error at or near
13441        // "INTO"`, which the differential found while measuring what
13442        // PostgreSQL tags each of the five materialising forms with. A
13443        // comment describing a capability the code does not have is the
13444        // defect this version has been finding all day, and this is the
13445        // one it found in the parser.
13446        //
13447        // `INTO` is captured rather than consumed here: the name has to
13448        // travel out of a function that returns a `SelectStatement`, and
13449        // the caller lowers the whole thing to the CTAS node.
13450        if matches!(self.peek(), Token::Into) {
13451            self.advance();
13452            // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13453            // the target, not part of its name. SPG has one storage
13454            // class, so `UNLOGGED` is accepted and means nothing, which
13455            // is what it already means on `CREATE TABLE`.
13456            let mut temporary = false;
13457            loop {
13458                match self.peek().clone() {
13459                    Token::Ident(w) | Token::QuotedIdent(w)
13460                        if w.eq_ignore_ascii_case("temp")
13461                            || w.eq_ignore_ascii_case("temporary") =>
13462                    {
13463                        temporary = true;
13464                        self.advance();
13465                    }
13466                    Token::Ident(w) | Token::QuotedIdent(w)
13467                        if w.eq_ignore_ascii_case("unlogged") =>
13468                    {
13469                        self.advance();
13470                    }
13471                    Token::Table => {
13472                        self.advance();
13473                    }
13474                    _ => break,
13475                }
13476            }
13477            let name = match self.peek().clone() {
13478                Token::Ident(w) | Token::QuotedIdent(w) => {
13479                    self.advance();
13480                    w
13481                }
13482                other => {
13483                    return Err(self.err(alloc::format!(
13484                        "expected a table name after SELECT … INTO, got {other:?}"
13485                    )));
13486                }
13487            };
13488            self.pending_select_into = Some((name, temporary));
13489        }
13490        // Scope the TABLESAMPLE lowering channel to this SELECT:
13491        // stash whatever an enclosing select accumulated, collect
13492        // our own FROM's predicates, restore after the combine.
13493        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13494        let mut from = if matches!(self.peek(), Token::From) {
13495            self.advance();
13496            Some(self.parse_from_clause()?)
13497        } else {
13498            None
13499        };
13500        // v7.37 D.22 — a set-returning function in the projection with no FROM
13501        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13502        // rows. Move the first SRF projection item to a FROM-position derived
13503        // table and replace it in the projection with a reference to its output
13504        // column; sibling scalar columns repeat per SRF row. PG names the output
13505        // column after the function (or its AS alias). Reuses the FROM-SRF
13506        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13507        // works via the targetlist-SRF path.
13508        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13509        // `SELECT * FROM f(args)` — the record's fields become the columns, which
13510        // is exactly what the function's own row shape already is. Anywhere else
13511        // (per outer row, or beside other items) it would need a real record-typed
13512        // projection, so it says so rather than answering something else.
13513        if let [
13514            SelectItem::Expr {
13515                expr: Expr::FunctionCall { name, args },
13516                ..
13517            },
13518        ] = items.as_slice()
13519            && name == "__record_expand"
13520        {
13521            let Some(Expr::FunctionCall {
13522                name: inner_name,
13523                args: inner_args,
13524            }) = args.first()
13525            else {
13526                return Err(self.err(
13527                    "(<expr>).* expands a function's record — it needs a function call".into(),
13528                ));
13529            };
13530            if from.is_some() {
13531                return Err(self.err(
13532                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13533                        .into(),
13534                ));
13535            }
13536            let fn_ref = TableRef {
13537                name: inner_name.clone(),
13538                alias: None,
13539                only: false,
13540                as_of_segment: None,
13541                unnest_expr: None,
13542                unnest_column_aliases: Vec::new(),
13543                with_ordinality: false,
13544                generate_series_args: None,
13545                lateral_subquery: None,
13546                jsonb_each_text_arg: None,
13547                table_fn_call: Some(Box::new((
13548                    inner_name.to_ascii_lowercase(),
13549                    inner_args.clone(),
13550                ))),
13551                rows_from: None,
13552                json_table: None,
13553                scalar_fn_item: false,
13554            };
13555            items = alloc::vec![SelectItem::Wildcard];
13556            from = Some(FromClause {
13557                primary: fn_ref,
13558                joins: Vec::new(),
13559            });
13560        }
13561        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13562        // FROM, keeps its marker: the ENGINE lowers it, because naming the
13563        // record's fields takes the catalog. It becomes a LATERAL of the same
13564        // function plus one item per declared column — the machinery rounds 65
13565        // and 69 already built.
13566        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13567        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13568        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13569        // express, since the lifted one becomes a scan and the other would
13570        // expand per its rows (a cross product, not a zip). So when the
13571        // projection holds more than one top-level function call, the lift steps
13572        // aside and the engine's target-list expansion takes the whole list.
13573        let fn_call_items = items
13574            .iter()
13575            .filter(|it| {
13576                matches!(
13577                    it,
13578                    SelectItem::Expr {
13579                        expr: Expr::FunctionCall { .. },
13580                        ..
13581                    }
13582                )
13583            })
13584            .count();
13585        if from.is_none() && fn_call_items <= 1 {
13586            let mut found: Option<(usize, TableRef, String)> = None;
13587            for (i, item) in items.iter().enumerate() {
13588                if let SelectItem::Expr {
13589                    expr: Expr::FunctionCall { name, args },
13590                    alias,
13591                } = item
13592                {
13593                    let lname = name.to_ascii_lowercase();
13594                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
13595                    let (unnest, gs) = match lname.as_str() {
13596                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13597                        "generate_series" if (2..=3).contains(&args.len()) => {
13598                            (None, Some(args.clone()))
13599                        }
13600                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
13601                        // no-FROM projection yields the 1-based subscripts, i.e.
13602                        // generate_series(1, array_length(arr, dim)); an invalid
13603                        // dimension makes array_length NULL → 0 rows, as in PG.
13604                        "generate_subscripts" if args.len() == 2 => (
13605                            None,
13606                            Some(alloc::vec![
13607                                Expr::Literal(Literal::Integer(1)),
13608                                Expr::FunctionCall {
13609                                    name: "array_length".to_string(),
13610                                    args: args.clone(),
13611                                },
13612                            ]),
13613                        ),
13614                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13615                        // in a no-FROM projection unnest their *_to_array form.
13616                        "string_to_table" | "regexp_split_to_table" => {
13617                            let array_fn = if lname == "string_to_table" {
13618                                "string_to_array"
13619                            } else {
13620                                "regexp_split_to_array"
13621                            };
13622                            (
13623                                Some(Box::new(Expr::FunctionCall {
13624                                    name: array_fn.to_string(),
13625                                    args: args.clone(),
13626                                })),
13627                                None,
13628                            )
13629                        }
13630                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13631                        // a no-FROM projection expand per element. The scalar form
13632                        // returns the elements as a TEXT array, so unnest over the
13633                        // same call materialises one row each (same rewrite the
13634                        // FROM-clause form uses).
13635                        "jsonb_array_elements"
13636                        | "json_array_elements"
13637                        | "jsonb_array_elements_text"
13638                        | "json_array_elements_text"
13639                            if args.len() == 1 =>
13640                        {
13641                            (
13642                                Some(Box::new(Expr::FunctionCall {
13643                                    name: lname.clone(),
13644                                    args: args.clone(),
13645                                })),
13646                                None,
13647                            )
13648                        }
13649                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13650                        // in a no-FROM projection expands per match (scalar form
13651                        // returns the matches as a TEXT array → unnest).
13652                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13653                            Some(Box::new(Expr::FunctionCall {
13654                                name: lname.clone(),
13655                                args: args.clone(),
13656                            })),
13657                            None,
13658                        ),
13659                        _ => continue,
13660                    };
13661                    found = Some((
13662                        i,
13663                        TableRef {
13664                            name: colname.clone(),
13665                            alias: Some(colname.clone()),
13666                            only: false,
13667                            as_of_segment: None,
13668                            unnest_expr: unnest,
13669                            unnest_column_aliases: alloc::vec![colname.clone()],
13670                            with_ordinality: false,
13671                            generate_series_args: gs,
13672                            lateral_subquery: None,
13673                            jsonb_each_text_arg: None,
13674                            table_fn_call: None,
13675                            rows_from: None,
13676                            json_table: None,
13677                            scalar_fn_item: false,
13678                        },
13679                        colname,
13680                    ));
13681                    break;
13682                }
13683            }
13684            if let Some((idx, tref, colname)) = found {
13685                from = Some(FromClause {
13686                    primary: tref,
13687                    joins: Vec::new(),
13688                });
13689                items[idx] = SelectItem::Expr {
13690                    expr: Expr::Column(ColumnName {
13691                        qualifier: None,
13692                        name: colname.clone(),
13693                    }),
13694                    alias: Some(colname),
13695                };
13696            }
13697        }
13698        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13699        let where_ = if matches!(self.peek(), Token::Where) {
13700            self.advance();
13701            Some(self.parse_expr(0)?)
13702        } else {
13703            None
13704        };
13705        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13706            Some(match acc {
13707                Some(w) => Expr::Binary {
13708                    lhs: Box::new(pred),
13709                    op: crate::ast::BinOp::And,
13710                    rhs: Box::new(w),
13711                },
13712                None => pred,
13713            })
13714        });
13715        self.pending_sample_preds = enclosing_sample_preds;
13716        let mut group_by_all = false;
13717        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13718        // share one expansion: `grouping_sets` lists the key subsets
13719        // (first = primary, assigned to stmt.group_by; the rest
13720        // become UNION ALL peers), `grouping_universe` is the full
13721        // key list used to compute each peer's dropped keys.
13722        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13723        let mut grouping_universe: Vec<Expr> = Vec::new();
13724        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13725        // A BOOL, not the key list: this frame is the statement parser's, and
13726        // round 430 measured that a `Vec` local here is enough on its own to
13727        // tip the 512 KiB nesting guard. The keys are recoverable from
13728        // `grouping_universe`, which a rollup fills with exactly them.
13729        let mut mysql_rollup = false;
13730        let group_by = if matches!(self.peek(), Token::Group) {
13731            self.advance();
13732            if !self.peek_is_by() {
13733                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13734            }
13735            self.advance();
13736            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13737            // every non-aggregate SELECT-list item later.
13738            if matches!(self.peek(), Token::All) {
13739                self.advance();
13740                group_by_all = true;
13741                None
13742            } else {
13743                // v7.39 (round 242) — PG's general grouping-element grammar:
13744                // GROUP BY [DISTINCT] element [, element]*, where an element
13745                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13746                // SETS (…) — mixed freely. Each element yields a list of
13747                // key sets; the query's grouping sets are the CARTESIAN
13748                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13749                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13750                // content. ROLLUP/CUBE members may be composite
13751                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13752                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13753                // parser handled only a lone ROLLUP/CUBE/GS as the whole
13754                // clause.
13755                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13756                    self.advance();
13757                    true
13758                } else {
13759                    false
13760                };
13761                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13762                loop {
13763                    element_sets.push(self.parse_grouping_element()?);
13764                    if matches!(self.peek(), Token::Comma) {
13765                        self.advance();
13766                    } else {
13767                        break;
13768                    }
13769                }
13770                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13771                for el in &element_sets {
13772                    let mut next: Vec<Vec<Expr>> = Vec::new();
13773                    for base in &total {
13774                        for set in el {
13775                            let mut merged = base.clone();
13776                            for k in set {
13777                                if !merged.iter().any(|m| m == k) {
13778                                    merged.push(k.clone());
13779                                }
13780                            }
13781                            next.push(merged);
13782                        }
13783                    }
13784                    total = next;
13785                }
13786                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13787                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13788                // The keys and the aggregates come out identical; the ROW
13789                // ORDER does not, and that is the part a report depends on.
13790                // MySQL interleaves each group's subtotal right after its
13791                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13792                // where the union-of-grouping-sets expansion emits every
13793                // leaf first and then every subtotal. MariaDB REFUSES an
13794                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13795                // order itself — measured on MariaDB 11 and MySQL 9.7, which
13796                // agree on the order and disagree only on whether ORDER BY
13797                // is allowed (MySQL allows it; SPG allows it too, since
13798                // refusing would break the clients that can write it).
13799                if self.mysql_dialect
13800                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13801                    && matches!(
13802                        self.tokens.get(self.pos + 1),
13803                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13804                    )
13805                {
13806                    self.advance(); // WITH
13807                    self.advance(); // ROLLUP
13808                    let keys = total.into_iter().next().unwrap_or_default();
13809                    mysql_rollup = true;
13810                    // n+1 prefixes, largest first — the same expansion
13811                    // `ROLLUP (…)` produces.
13812                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13813                }
13814                if distinct_sets {
13815                    let mut seen: Vec<Vec<String>> = Vec::new();
13816                    total.retain(|set| {
13817                        let mut key: Vec<String> =
13818                            set.iter().map(|e| alloc::format!("{e}")).collect();
13819                        key.sort();
13820                        if seen.contains(&key) {
13821                            false
13822                        } else {
13823                            seen.push(key);
13824                            true
13825                        }
13826                    });
13827                }
13828                if total.len() > 1 {
13829                    let mut universe: Vec<Expr> = Vec::new();
13830                    for set in &total {
13831                        for k in set {
13832                            if !universe.iter().any(|u| u == k) {
13833                                universe.push(k.clone());
13834                            }
13835                        }
13836                    }
13837                    grouping_universe = universe;
13838                    let primary = total[0].clone();
13839                    grouping_sets = total;
13840                    Some(primary)
13841                } else {
13842                    // One set (a plain GROUP BY list, or a single-set
13843                    // spelling like GROUPING SETS ((a, b))). An EMPTY
13844                    // single set — GROUPING SETS (()) — stays
13845                    // `Some(vec![])`: the grand-total group, which must
13846                    // run the aggregate path.
13847                    Some(total.into_iter().next().unwrap_or_default())
13848                }
13849            }
13850        } else {
13851            None
13852        };
13853        let having = if matches!(self.peek(), Token::Having) {
13854            self.advance();
13855            Some(self.parse_expr(0)?)
13856        } else {
13857            None
13858        };
13859        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13860        // OVER w parsed to a marker above; inline each definition
13861        // into the referencing WindowFunction nodes.
13862        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13863        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13864            self.advance();
13865            loop {
13866                let wname = self.expect_ident_like()?;
13867                if !matches!(self.peek(), Token::As) {
13868                    return Err(self.err(format!(
13869                        "expected AS after WINDOW {wname}, got {:?}",
13870                        self.peek()
13871                    )));
13872                }
13873                self.advance();
13874                // v7.39 (round 229) — PG rejects a redefinition outright.
13875                if window_defs
13876                    .iter()
13877                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13878                {
13879                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13880                }
13881                let def = self.parse_over_clause()?;
13882                // A definition may itself copy an earlier one
13883                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13884                // so resolve it against the defs already in scope. Same
13885                // copy rules as an `OVER (w1 …)` in the select list.
13886                let mut probe = Expr::WindowFunction {
13887                    name: String::new(),
13888                    args: Vec::new(),
13889                    partition_by: def.0,
13890                    order_by: def.1,
13891                    frame: def.2,
13892                    null_treatment: crate::ast::NullTreatment::Respect,
13893                    filter: None,
13894                };
13895                Self::substitute_named_windows(&mut probe, &window_defs)
13896                    .map_err(|m| self.err(m))?;
13897                let Expr::WindowFunction {
13898                    partition_by,
13899                    order_by,
13900                    frame,
13901                    ..
13902                } = probe
13903                else {
13904                    unreachable!("probe is a WindowFunction")
13905                };
13906                window_defs.push((wname, (partition_by, order_by, frame)));
13907                if matches!(self.peek(), Token::Comma) {
13908                    self.advance();
13909                    continue;
13910                }
13911                break;
13912            }
13913        }
13914        // v7.39 (round 705) — which definitions did anything reference?
13915        // The ones nothing did used to be dropped here, unexamined, so
13916        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13917        // definition whether referenced or not. Their key expressions ride
13918        // out on the statement for the engine to resolve.
13919        let mut window_refs: Vec<String> = Vec::new();
13920        if !window_defs.is_empty() {
13921            for it in &items {
13922                if let SelectItem::Expr { expr, .. } = it {
13923                    Self::collect_named_window_refs(expr, &mut window_refs);
13924                }
13925            }
13926        }
13927        let window_check_exprs: Vec<Expr> = window_defs
13928            .iter()
13929            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13930            .flat_map(|(_, (partition, order, _))| {
13931                partition
13932                    .iter()
13933                    .cloned()
13934                    .chain(order.iter().map(|(e, _, _)| e.clone()))
13935            })
13936            .collect();
13937        if !window_defs.is_empty()
13938            || items
13939                .iter()
13940                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13941        {
13942            for it in &mut items {
13943                if let SelectItem::Expr { expr, .. } = it {
13944                    Self::substitute_named_windows(expr, &window_defs)
13945                        .map_err(|m| self.err(m))?;
13946                }
13947            }
13948        }
13949        // `GROUP BY 1` — positional keys substitute with the Nth
13950        // select item's expression (same contract ORDER BY has had
13951        // since v6.x). Out-of-range positions error.
13952        let group_by = match group_by {
13953            Some(mut keys) => {
13954                for k in &mut keys {
13955                    if let Expr::Literal(Literal::Integer(n)) = k {
13956                        let idx = *n;
13957                        if idx < 1 || idx as usize > items.len() {
13958                            return Err(self.err(alloc::format!(
13959                                "GROUP BY position {idx} is not in select list"
13960                            )));
13961                        }
13962                        match &items[(idx - 1) as usize] {
13963                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
13964                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13965                                return Err(self.err(alloc::format!(
13966                                    "GROUP BY position {idx} references a wildcard item"
13967                                )));
13968                            }
13969                        }
13970                    }
13971                }
13972                Some(keys)
13973            }
13974            None => None,
13975        };
13976        let mut stmt = SelectStatement {
13977            locking: None,
13978            ctes: Vec::new(),
13979            distinct,
13980            distinct_on,
13981            items,
13982            from,
13983            where_,
13984            group_by,
13985            group_by_all,
13986            having,
13987            unions: Vec::new(),
13988            order_by: Vec::new(),
13989            limit: None,
13990            offset: None,
13991            limit_with_ties: false,
13992            window_check_exprs,
13993        };
13994        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13995        // first set is the primary (already on stmt.group_by); each
13996        // further set becomes a UNION ALL peer with its dropped
13997        // keys (universe minus the set) replaced by NULL literals
13998        // in the peer's items and group_by. PG-legal: non-grouped
13999        // select items must be group keys or aggregates, so a
14000        // dropped key's occurrences in the projection are exactly
14001        // the ones to nullify.
14002        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14003        // over a plain GROUP BY (every argument must be a group key; the
14004        // mask is then 0) and rejects anything else with 42803. SPG's
14005        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14006        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14007        // function `grouping`".
14008        if grouping_sets.len() <= 1 {
14009            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14010            let mut calls: Vec<Expr> = Vec::new();
14011            for item in &stmt.items {
14012                if let SelectItem::Expr { expr, .. } = item {
14013                    Self::collect_grouping_calls(expr, &mut calls);
14014                }
14015            }
14016            if let Some(h) = &stmt.having {
14017                Self::collect_grouping_calls(h, &mut calls);
14018            }
14019            for call in &calls {
14020                let Expr::FunctionCall { args, .. } = call else {
14021                    continue;
14022                };
14023                for a in args {
14024                    if !keys.iter().any(|k| k == a) {
14025                        return Err(self.err(
14026                            "arguments to GROUPING must be grouping expressions of the associated query level"
14027                                .to_string(),
14028                        ));
14029                    }
14030                }
14031            }
14032            if !calls.is_empty() {
14033                for item in &mut stmt.items {
14034                    if let SelectItem::Expr { expr, .. } = item {
14035                        Self::substitute_grouping_calls(expr, &[]);
14036                    }
14037                }
14038                if let Some(h) = &mut stmt.having {
14039                    Self::substitute_grouping_calls(h, &[]);
14040                }
14041            }
14042        }
14043        if grouping_sets.len() > 1 {
14044            // The primary set's own dropped keys nullify in the
14045            // HEAD's projection too (GROUPING SETS's first set may
14046            // omit keys other sets use).
14047            let primary = grouping_sets[0].clone();
14048            let head_dropped: Vec<Expr> = grouping_universe
14049                .iter()
14050                .filter(|u| !primary.iter().any(|k| k == *u))
14051                .cloned()
14052                .collect();
14053            for set in grouping_sets.iter().skip(1) {
14054                let mut peer = stmt.clone();
14055                peer.unions = Vec::new();
14056                let dropped: Vec<&Expr> = grouping_universe
14057                    .iter()
14058                    .filter(|u| !set.iter().any(|k| k == *u))
14059                    .collect();
14060                // Empty set = grand-total group: `Some(vec![])` forces
14061                // the aggregate path (one collapsed row) instead of a
14062                // per-row passthrough. See the primary-set note above.
14063                peer.group_by = Some(set.clone());
14064                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14065                for item in &mut peer.items {
14066                    if let SelectItem::Expr { expr, alias } = item {
14067                        if dropped.iter().any(|d| *d == expr) {
14068                            // v7.39 — keep the dropped key's name on the
14069                            // NULL literal so the UNION output column
14070                            // (and any top-level ORDER BY on it) still
14071                            // resolves.
14072                            if alias.is_none()
14073                                && let Expr::Column(c) = &expr
14074                            {
14075                                *alias = Some(c.name.clone());
14076                            }
14077                            *expr = Expr::Literal(Literal::Null);
14078                        } else {
14079                            Self::substitute_grouping_calls(expr, &dropped_owned);
14080                        }
14081                    }
14082                }
14083                if let Some(h) = &mut peer.having {
14084                    Self::substitute_grouping_calls(h, &dropped_owned);
14085                }
14086                stmt.unions.push((UnionKind::All, peer));
14087            }
14088            for item in &mut stmt.items {
14089                if let SelectItem::Expr { expr, alias } = item {
14090                    if head_dropped.iter().any(|d| d == expr) {
14091                        if alias.is_none()
14092                            && let Expr::Column(c) = &expr
14093                        {
14094                            *alias = Some(c.name.clone());
14095                        }
14096                        *expr = Expr::Literal(Literal::Null);
14097                    } else {
14098                        Self::substitute_grouping_calls(expr, &head_dropped);
14099                    }
14100                }
14101            }
14102            if let Some(h) = &mut stmt.having {
14103                Self::substitute_grouping_calls(h, &head_dropped);
14104            }
14105            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14106            // (while `grouping_universe` / the per-branch sets are in scope). For
14107            // each grouping() call in it, inject a per-branch hidden column
14108            // `__grp_ord_K` carrying that branch's mask into the head + every
14109            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14110            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14111            // from the final output. A standalone grouping-set query has ORDER BY
14112            // (not an explicit set-op) next, so consuming it here is safe.
14113            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14114            // rollup carries the hierarchical order: sort by the grouping
14115            // keys with the rolled-up NULLs last, which is exactly the
14116            // interleaving both oracles emit. A client's own ORDER BY wins,
14117            // which is what MySQL does (MariaDB refuses to let one be
14118            // written at all).
14119            // The synthesised keys have to travel the SAME path a written
14120            // ORDER BY does: the block below is what turns a `grouping()`
14121            // call into the per-branch `__grp_ord_K` column the engine can
14122            // actually sort on. Bypassing it left a bare `grouping(text)`
14123            // for the evaluator to reject.
14124            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14125                self.parse_order_by_keys()?
14126            } else if mysql_rollup {
14127                Self::mysql_rollup_order(&grouping_universe)
14128            } else {
14129                Vec::new()
14130            };
14131            if !synthesised_or_parsed.is_empty() {
14132                let mut order_keys = synthesised_or_parsed;
14133                let mut grp_exprs: Vec<Expr> = Vec::new();
14134                for ob in &order_keys {
14135                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14136                }
14137                for (k, gexpr) in grp_exprs.iter().enumerate() {
14138                    let colname = alloc::format!("__grp_ord_{k}");
14139                    // Head branch (primary set) uses `head_dropped`.
14140                    let mut he = gexpr.clone();
14141                    Self::substitute_grouping_calls(&mut he, &head_dropped);
14142                    stmt.items.push(SelectItem::Expr {
14143                        expr: he,
14144                        alias: Some(colname.clone()),
14145                    });
14146                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14147                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14148                        let set = &grouping_sets[i + 1];
14149                        let dropped: Vec<Expr> = grouping_universe
14150                            .iter()
14151                            .filter(|u| !set.iter().any(|k| k == *u))
14152                            .cloned()
14153                            .collect();
14154                        let mut pe = gexpr.clone();
14155                        Self::substitute_grouping_calls(&mut pe, &dropped);
14156                        peer.items.push(SelectItem::Expr {
14157                            expr: pe,
14158                            alias: Some(colname.clone()),
14159                        });
14160                    }
14161                }
14162                for ob in &mut order_keys {
14163                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14164                }
14165                stmt.order_by = order_keys;
14166            }
14167        }
14168        Ok(stmt)
14169    }
14170
14171    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14172    /// as ORDER BY keys.
14173    ///
14174    /// Per key: the rollup marker, then the key. Sorting on the key alone
14175    /// is not enough, and a table with a NULL in it says why — MariaDB puts
14176    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14177    /// the ROLLUP-introduced NULL last, and both print as NULL.
14178    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14179    /// real group including the data-NULL one, 1 only for the row the
14180    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14181    /// rolls up to NULL|2, a|1, b|3, NULL|6.
14182    ///
14183    /// `#[inline(never)]`: its locals must not join the statement parser's
14184    /// frame, which round 430 measured sitting against the nesting guard.
14185    #[inline(never)]
14186    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14187        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14188        for e in keys {
14189            out.push(OrderBy {
14190                expr: Expr::FunctionCall {
14191                    name: "grouping".into(),
14192                    args: alloc::vec![e.clone()],
14193                },
14194                desc: false,
14195                nulls_first: None,
14196                collation: None,
14197            });
14198            out.push(OrderBy {
14199                expr: e.clone(),
14200                desc: false,
14201                // MySQL orders NULL first on an ascending key.
14202                nulls_first: Some(true),
14203                collation: None,
14204            });
14205        }
14206        out
14207    }
14208
14209    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14210    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14211    #[inline(never)]
14212    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14213        use crate::ast::MaintainKind;
14214        self.skip_paren_option_list();
14215        let kind = match self.peek() {
14216            // `TABLE` and `INDEX` lex as keywords, not identifiers.
14217            Token::Table | Token::Index => {
14218                self.advance();
14219                MaintainKind::ReindexRelation
14220            }
14221            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14222                "index" | "table" => {
14223                    self.advance();
14224                    MaintainKind::ReindexRelation
14225                }
14226                "schema" => {
14227                    self.advance();
14228                    MaintainKind::ReindexSchema
14229                }
14230                "system" | "database" => {
14231                    self.advance();
14232                    MaintainKind::Whole
14233                }
14234                // PG requires the object type; anything else is the
14235                // caller's problem, not something to swallow.
14236                _ => MaintainKind::ReindexRelation,
14237            },
14238            _ => MaintainKind::Whole,
14239        };
14240        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14241        // allows the plain form, so the modifier is recorded rather than
14242        // skipped. It still has no effect on how the reindex runs.
14243        let mut concurrently = false;
14244        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14245            self.advance();
14246            concurrently = true;
14247        }
14248        let target = self.take_optional_maintain_name();
14249        self.consume_until_statement_boundary();
14250        Ok(Statement::Maintain {
14251            kind,
14252            concurrently,
14253            target,
14254        })
14255    }
14256
14257    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14258    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14259    #[inline(never)]
14260    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14261        use crate::ast::MaintainKind;
14262        self.skip_paren_option_list();
14263        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14264            self.advance();
14265        }
14266        let target = self.take_optional_maintain_name();
14267        self.consume_until_statement_boundary();
14268        Ok(Statement::Maintain {
14269            kind: if target.is_some() {
14270                MaintainKind::ClusterRelation
14271            } else {
14272                MaintainKind::Whole
14273            },
14274            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14275            // transaction block quite happily (measured).
14276            concurrently: false,
14277            target,
14278        })
14279    }
14280
14281    /// The next token as a relation / schema name, when there is one.
14282    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14283        match self.peek() {
14284            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14285                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14286                _ => None,
14287            },
14288            _ => None,
14289        }
14290    }
14291
14292    /// A parenthesised option list, absorbed.
14293    fn skip_paren_option_list(&mut self) {
14294        if !matches!(self.peek(), Token::LParen) {
14295            return;
14296        }
14297        let mut depth = 0usize;
14298        loop {
14299            match self.advance() {
14300                Token::LParen => depth += 1,
14301                Token::RParen => {
14302                    depth -= 1;
14303                    if depth == 0 {
14304                        return;
14305                    }
14306                }
14307                Token::Eof => return,
14308                _ => {}
14309            }
14310        }
14311    }
14312
14313    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14314    /// column list.
14315    ///
14316    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14317    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14318    /// / ALL. The three that describe physical storage have no meaning
14319    /// here, so they parse and change nothing rather than making a
14320    /// dump that mentions them fail to load.
14321    ///
14322    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14323    /// parse chain the nesting sentinel is tuned against.
14324    #[inline(never)]
14325    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14326        self.advance(); // LIKE
14327        let source = self.expect_ident_like()?;
14328        let mut options = crate::ast::LikeOptions::default();
14329        loop {
14330            let including = match self.peek() {
14331                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14332                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14333                _ => break,
14334            };
14335            self.advance();
14336            // `ALL` lexes as its own keyword, not an identifier.
14337            let opt = if matches!(self.peek(), Token::All) {
14338                self.advance();
14339                alloc::string::String::from("all")
14340            } else {
14341                self.expect_ident_like()?
14342            };
14343            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14344                o.defaults = on;
14345                o.constraints = on;
14346                o.identity = on;
14347                o.generated = on;
14348                o.indexes = on;
14349                o.comments = on;
14350            };
14351            match opt.to_ascii_lowercase().as_str() {
14352                "all" => set(&mut options, including),
14353                "defaults" => options.defaults = including,
14354                "constraints" => options.constraints = including,
14355                "identity" => options.identity = including,
14356                "generated" => options.generated = including,
14357                "indexes" => options.indexes = including,
14358                "comments" => options.comments = including,
14359                // No storage model to copy into.
14360                "storage" | "statistics" | "compression" => {}
14361                other => {
14362                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14363                }
14364            }
14365        }
14366        Ok(crate::ast::LikeSpec {
14367            source,
14368            at,
14369            options,
14370        })
14371    }
14372
14373    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14374        // Caller already consumed CREATE; we're sitting on TABLE.
14375        debug_assert!(matches!(self.peek(), Token::Table));
14376        self.advance();
14377        let if_not_exists = self.consume_if_not_exists();
14378        let name = self.expect_ident_like()?;
14379        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14380        // child shape has no column list; the child inherits its
14381        // columns from the parent at engine-DDL time. Detect it
14382        // before the `(` requirement below.
14383        if matches!(self.peek(), Token::Partition)
14384            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14385        {
14386            self.advance(); // PARTITION
14387            self.advance(); // of
14388            let partition_of = self.parse_partition_of_tail()?;
14389            return Ok(Statement::CreateTable(CreateTableStatement {
14390                temporary: false,
14391                name,
14392                columns: Vec::new(),
14393                like_specs: Vec::new(),
14394                inherits: Vec::new(),
14395                if_not_exists,
14396                foreign_keys: Vec::new(),
14397                table_constraints: Vec::new(),
14398                partition_by: None,
14399                partition_of: Some(partition_of),
14400            }));
14401        }
14402        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14403        // the materialized-view materialisation path (run the SELECT, infer the
14404        // column types, create + populate the table) but marks the node so the
14405        // executor creates a plain table without a mat-view registry entry.
14406        if matches!(self.peek(), Token::As) {
14407            self.advance();
14408            let body_stmt = self.parse_select_stmt()?;
14409            let Statement::Select(body) = body_stmt else {
14410                return Err(self.err(format!(
14411                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14412                )));
14413            };
14414            let with_data = self.parse_optional_with_data(true)?;
14415            return Ok(Statement::CreateMaterializedView(
14416                crate::ast::CreateMaterializedViewStatement {
14417                    temporary: false,
14418                    name,
14419                    if_not_exists,
14420                    columns: Vec::new(),
14421                    body,
14422                    with_data,
14423                    as_plain_table: true,
14424                },
14425            ));
14426        }
14427        if !matches!(self.peek(), Token::LParen) {
14428            return Err(self.err(format!(
14429                "expected '(' after table name, got {:?}",
14430                self.peek()
14431            )));
14432        }
14433        self.advance();
14434        let mut columns = Vec::new();
14435        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14436        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14437        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14438        loop {
14439            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14440            // column list. It is how a child that adds nothing of its own is
14441            // written, and this loop demanded at least one entry: `syntax
14442            // error at or near ")"`. The child takes the parent's columns,
14443            // which the INHERITS clause already arranges.
14444            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14445                self.advance();
14446                break;
14447            }
14448            // v7.6.0 / v7.9.18 — distinguish table-level constraint
14449            // clauses from column definitions. Constraints start
14450            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14451            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14452            // a column.
14453            if self.peek_table_level_pk_start() {
14454                table_constraints.push(self.parse_table_level_primary_key()?);
14455            } else if matches!(self.peek(), Token::Like) {
14456                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14457                // <opt> ]*`. The source table's shape lives in the catalog,
14458                // so this records the clause and the engine expands it.
14459                like_specs.push(self.parse_create_table_like(columns.len())?);
14460            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14461                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14462                table_constraints.push(self.parse_table_level_exclude()?);
14463            } else if self.peek_table_level_unique_start() {
14464                table_constraints.push(self.parse_table_level_unique()?);
14465            } else if self.peek_table_level_check_start() {
14466                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14467                table_constraints.push(self.parse_table_level_check()?);
14468            } else if self.peek_mysql_inline_key_start() {
14469                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14470                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14471                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14472                // inside the column list. Skip name + paren list;
14473                // for UNIQUE KEY, register as a UC.
14474                if let Some(uc) = self.parse_mysql_inline_key()? {
14475                    table_constraints.push(uc);
14476                }
14477            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14478                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14479                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14480                // CHECK is named, and the named-CONSTRAINT arm used
14481                // to accept FOREIGN KEY only. The name is accepted
14482                // and discarded — same handling as every other SPG
14483                // constraint name.
14484                self.advance(); // CONSTRAINT
14485                // v7.39 (read01 round 48) — the name is kept now: the schema
14486                // stores it, so DROP / RENAME CONSTRAINT can find it.
14487                let con_name = self.expect_ident_like()?;
14488                let mut tc = match kind {
14489                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14490                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14491                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14492                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14493                };
14494                match &mut tc {
14495                    crate::ast::TableConstraint::Check { name, .. }
14496                    | crate::ast::TableConstraint::Unique { name, .. }
14497                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
14498                    | crate::ast::TableConstraint::Exclude { name, .. } => {
14499                        *name = Some(con_name);
14500                    }
14501                    _ => {}
14502                }
14503                table_constraints.push(tc);
14504            } else if self.peek_constraint_or_fk_start() {
14505                foreign_keys.push(self.parse_table_level_fk()?);
14506            } else {
14507                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14508                // v7.13.0 — fold inline UNIQUE / CHECK column
14509                // constraints into table-level entries so the
14510                // engine path stays uniform.
14511                if col.is_unique {
14512                    table_constraints.push(crate::ast::TableConstraint::Unique {
14513                        name: None,
14514                        columns: alloc::vec![col.name.clone()],
14515                        nulls_not_distinct: col.unique_nulls_not_distinct,
14516                        deferrable: col.constraint_deferrable,
14517                        initially_deferred: col.constraint_initially_deferred,
14518                    });
14519                }
14520                if let Some(check_expr) = col.check.clone() {
14521                    table_constraints.push(crate::ast::TableConstraint::Check {
14522                        name: None,
14523                        expr: check_expr,
14524                        not_valid: false,
14525                    });
14526                }
14527                columns.push(col);
14528                if let Some(fk) = col_level_fk {
14529                    foreign_keys.push(fk);
14530                }
14531            }
14532            match self.peek() {
14533                Token::Comma => {
14534                    self.advance();
14535                }
14536                Token::RParen => {
14537                    self.advance();
14538                    break;
14539                }
14540                other => {
14541                    return Err(
14542                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14543                    );
14544                }
14545            }
14546        }
14547        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14548        // `CREATE TABLE k (LIKE t)` is a complete definition even though
14549        // nothing is written between the parentheses.
14550        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14551        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14552        // empty parentheses were a parse error in their own right — quite apart
14553        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14554        // SPG does not have (filed separately).
14555        let _ = &like_specs;
14556        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14557        // It sits between the column list and the MySQL table options,
14558        // and it was a syntax error until this round.
14559        let mut inherits: Vec<String> = Vec::new();
14560        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14561            if k.eq_ignore_ascii_case("inherits"))
14562        {
14563            self.advance();
14564            if !matches!(self.peek(), Token::LParen) {
14565                return Err(self.err(alloc::format!(
14566                    "expected ( after INHERITS, got {:?}",
14567                    self.peek()
14568                )));
14569            }
14570            self.advance();
14571            loop {
14572                inherits.push(self.expect_ident_like()?);
14573                if matches!(self.peek(), Token::Comma) {
14574                    self.advance();
14575                    continue;
14576                }
14577                break;
14578            }
14579            if !matches!(self.peek(), Token::RParen) {
14580                return Err(self.err(alloc::format!(
14581                    "expected ) closing INHERITS, got {:?}",
14582                    self.peek()
14583                )));
14584            }
14585            self.advance();
14586        }
14587        // v7.14.0 — consume MySQL/MariaDB table options after the
14588        // closing `)`. mysqldump emits things like
14589        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14590        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14591        // SPG accepts all forms as no-ops (each option is
14592        // `<ident> [=] <ident-or-string>` separated by whitespace).
14593        self.consume_mysql_table_options();
14594        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14595        // SPG has no per-table reloptions, so accept and ignore them so a
14596        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14597        self.consume_with_reloptions();
14598        // v7.37.6-B — declarative-partition-parent suffix
14599        // (`PARTITION BY RANGE (key_col)`) sits after the column
14600        // list + MySQL table-options. v7.37.6-B only accepts RANGE
14601        // and locks the key column at one ident; the engine then
14602        // verifies the column type is TIMESTAMPTZ.
14603        let partition_by = if matches!(self.peek(), Token::Partition) {
14604            self.advance(); // PARTITION
14605            if !self.peek_is_by() {
14606                return Err(self.err(format!(
14607                    "expected BY after PARTITION, got {:?}",
14608                    self.peek()
14609                )));
14610            }
14611            self.advance();
14612            Some(self.parse_partition_by_tail()?)
14613        } else {
14614            None
14615        };
14616        Ok(Statement::CreateTable(CreateTableStatement {
14617            temporary: false,
14618            name,
14619            columns,
14620            like_specs,
14621            inherits,
14622            if_not_exists,
14623            foreign_keys,
14624            table_constraints,
14625            partition_by,
14626            partition_of: None,
14627        }))
14628    }
14629
14630    /// v7.37.6-B — case-insensitive ident match helper for the
14631    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14632    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14633    /// didn't burn a global keyword slot for each (see the
14634    /// `Token::Partition` doc-comment in `lexer.rs`).
14635    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14636        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14637    }
14638
14639    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14640    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14641    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14642        use crate::ast::{PartitionBySpec, PartitionKindAst};
14643        let kind = match self.peek() {
14644            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14645                self.advance();
14646                PartitionKindAst::Range
14647            }
14648            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14649                self.advance();
14650                PartitionKindAst::List
14651            }
14652            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14653                self.advance();
14654                PartitionKindAst::Hash
14655            }
14656            other => {
14657                return Err(self.err(format!(
14658                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14659                )));
14660            }
14661        };
14662        if !matches!(self.peek(), Token::LParen) {
14663            return Err(self.err(format!(
14664                "expected '(' after PARTITION BY <strategy>, got {:?}",
14665                self.peek()
14666            )));
14667        }
14668        self.advance();
14669        let mut key_columns = Vec::new();
14670        loop {
14671            key_columns.push(self.expect_ident_like()?);
14672            match self.peek() {
14673                Token::Comma => {
14674                    self.advance();
14675                }
14676                Token::RParen => {
14677                    self.advance();
14678                    break;
14679                }
14680                other => {
14681                    return Err(self.err(format!(
14682                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14683                    )));
14684                }
14685            }
14686        }
14687        if key_columns.is_empty() {
14688            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14689        }
14690        Ok(PartitionBySpec { kind, key_columns })
14691    }
14692
14693    /// v7.37.6-B — after `PARTITION OF`, expect
14694    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14695    /// or
14696    ///   <parent> DEFAULT
14697    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14698        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14699        let parent_name = self.expect_ident_like()?;
14700        // v7.37.6-B rejects an explicit column list — the child
14701        // inherits from the parent. mailrs round-7 taught us that
14702        // CREATE TABLE-side schema reconciliation hides drift, so
14703        // we surface this as a parse error rather than silently
14704        // ignoring user columns.
14705        if matches!(self.peek(), Token::LParen) {
14706            return Err(self.err(
14707                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14708                 at v7.37.6-B; the child inherits its columns from the parent"
14709                    .to_string(),
14710            ));
14711        }
14712        let bounds = match self.peek() {
14713            Token::Default => {
14714                self.advance();
14715                PartitionOfBoundsAst::Default
14716            }
14717            Token::For => {
14718                self.advance();
14719                if !matches!(self.peek(), Token::Values) {
14720                    return Err(
14721                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14722                    );
14723                }
14724                self.advance();
14725                // WITH is not a reserved Token in the lexer — it lexes
14726                // as Token::Ident("with"). Disambiguate manually.
14727                let want_with = matches!(
14728                    self.peek(),
14729                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14730                );
14731                if want_with {
14732                    self.advance();
14733                    if !matches!(self.peek(), Token::LParen) {
14734                        return Err(self.err(format!(
14735                            "expected '(' after FOR VALUES WITH, got {:?}",
14736                            self.peek()
14737                        )));
14738                    }
14739                    self.advance();
14740                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14741                    loop {
14742                        let key = self.expect_ident_like()?;
14743                        let n = match self.peek().clone() {
14744                            Token::Integer(v) if u32::try_from(v).is_ok() => {
14745                                self.advance();
14746                                v as u32
14747                            }
14748                            other => {
14749                                return Err(self.err(format!(
14750                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14751                                )));
14752                            }
14753                        };
14754                        match key.to_ascii_uppercase().as_str() {
14755                            "MODULUS" => modulus = Some(n),
14756                            "REMAINDER" => remainder = Some(n),
14757                            other => {
14758                                return Err(self.err(format!(
14759                                    "FOR VALUES WITH: unknown key {other:?}; \
14760                                     expected MODULUS or REMAINDER"
14761                                )));
14762                            }
14763                        }
14764                        match self.peek() {
14765                            Token::Comma => {
14766                                self.advance();
14767                            }
14768                            Token::RParen => {
14769                                self.advance();
14770                                break;
14771                            }
14772                            other => {
14773                                return Err(self.err(format!(
14774                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14775                                )));
14776                            }
14777                        }
14778                    }
14779                    let modulus = modulus
14780                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14781                    let remainder = remainder.ok_or_else(|| {
14782                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14783                    })?;
14784                    if modulus == 0 {
14785                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14786                    }
14787                    if remainder >= modulus {
14788                        return Err(self.err(format!(
14789                            "FOR VALUES WITH: REMAINDER ({remainder}) \
14790                             must be < MODULUS ({modulus})"
14791                        )));
14792                    }
14793                    PartitionOfBoundsAst::Hash { modulus, remainder }
14794                } else {
14795                    match self.peek() {
14796                        Token::From => {
14797                            self.advance();
14798                            let lower = Box::new(self.parse_partition_bound_expr()?);
14799                            if !matches!(self.peek(), Token::To) {
14800                                return Err(self.err(format!(
14801                                    "expected TO after FROM (...), got {:?}",
14802                                    self.peek()
14803                                )));
14804                            }
14805                            self.advance();
14806                            let upper = Box::new(self.parse_partition_bound_expr()?);
14807                            PartitionOfBoundsAst::Range { lower, upper }
14808                        }
14809                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14810                        Token::In => {
14811                            self.advance();
14812                            if !matches!(self.peek(), Token::LParen) {
14813                                return Err(self.err(format!(
14814                                    "expected '(' after FOR VALUES IN, got {:?}",
14815                                    self.peek()
14816                                )));
14817                            }
14818                            self.advance();
14819                            let mut values = Vec::new();
14820                            loop {
14821                                values.push(self.parse_expr(0)?);
14822                                match self.peek() {
14823                                    Token::Comma => {
14824                                        self.advance();
14825                                    }
14826                                    Token::RParen => {
14827                                        self.advance();
14828                                        break;
14829                                    }
14830                                    other => {
14831                                        return Err(self.err(format!(
14832                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14833                                    )));
14834                                    }
14835                                }
14836                            }
14837                            if values.is_empty() {
14838                                return Err(self.err(
14839                                    "FOR VALUES IN requires at least one literal".to_string(),
14840                                ));
14841                            }
14842                            PartitionOfBoundsAst::List { values }
14843                        }
14844                        other => {
14845                            return Err(self.err(format!(
14846                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14847                            )));
14848                        }
14849                    }
14850                }
14851            }
14852            other => {
14853                return Err(self.err(format!(
14854                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14855                )));
14856            }
14857        };
14858        Ok(PartitionOfSpec {
14859            parent_name,
14860            bounds,
14861        })
14862    }
14863
14864    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14865    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14866    /// markers (no-arg builtins) so the engine resolves them
14867    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14868    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14869        if !matches!(self.peek(), Token::LParen) {
14870            return Err(self.err(format!(
14871                "expected '(' before partition bound, got {:?}",
14872                self.peek()
14873            )));
14874        }
14875        self.advance();
14876        let expr = match self.peek() {
14877            Token::Ident(s) | Token::QuotedIdent(s)
14878                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14879            {
14880                let name = s.to_ascii_uppercase();
14881                self.advance();
14882                crate::ast::Expr::FunctionCall {
14883                    name,
14884                    args: Vec::new(),
14885                }
14886            }
14887            _ => self.parse_expr(0)?,
14888        };
14889        if !matches!(self.peek(), Token::RParen) {
14890            return Err(self.err(format!(
14891                "expected ')' after partition bound, got {:?}",
14892                self.peek()
14893            )));
14894        }
14895        self.advance();
14896        Ok(expr)
14897    }
14898
14899    /// v7.14.0 — true when the next tokens look like an inline
14900    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14901    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14902    /// — each followed by an optional name + `(...)`. Critical:
14903    /// a column NAMED `key` / `index` (PG accepts as ident) must
14904    /// NOT be mistaken for the KEY constraint shape. We disambig
14905    /// by requiring the keyword to be followed by either `(` or
14906    /// `<ident> (`.
14907    fn peek_mysql_inline_key_start(&self) -> bool {
14908        let cur = self.peek();
14909        // Shapes:
14910        //   KEY (cols)
14911        //   KEY name (cols)
14912        //   INDEX (cols)
14913        //   INDEX name (cols)
14914        //   UNIQUE KEY [name] (cols)
14915        //   UNIQUE INDEX [name] (cols)
14916        //   FULLTEXT [KEY|INDEX] [name] (cols)
14917        //   SPATIAL [KEY|INDEX] [name] (cols)
14918        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14919            // tokens at skip = the position AFTER the index-form
14920            // keywords (KEY/INDEX) have been consumed.
14921            match self.tokens.get(skip) {
14922                Some(Token::LParen) => true,
14923                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14924                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14925                }
14926                _ => false,
14927            }
14928        };
14929        // `INDEX` lexes as Token::Index (reserved), not as
14930        // Token::Ident("index"). Both shapes count as a KEY/INDEX
14931        // start; the peek helper below handles either.
14932        let is_key_or_index_tok = |t: &Token| -> bool {
14933            matches!(t, Token::Index)
14934                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14935        };
14936        match cur {
14937            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14938            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14939                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14940            }
14941            Token::Ident(s)
14942                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14943            {
14944                let nxt = self.tokens.get(self.pos + 1);
14945                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14946                    self.pos + 2
14947                } else {
14948                    self.pos + 1
14949                };
14950                after_keyword_followed_by_paren_or_ident_paren(after_after)
14951            }
14952            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14953                let nxt = self.tokens.get(self.pos + 1);
14954                if !nxt.is_some_and(is_key_or_index_tok) {
14955                    return false;
14956                }
14957                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14958            }
14959            _ => false,
14960        }
14961    }
14962
14963    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14964    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14965    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14966    /// returns Some(TableConstraint::Index) so the engine builds
14967    /// a real BTree index on the leading column (mysqldump
14968    /// `KEY idx_posts_author (author_id)` shape).
14969    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14970    /// (the storage layer has no matching AM).
14971    fn parse_mysql_inline_key(
14972        &mut self,
14973    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14974        // Detect UNIQUE prefix.
14975        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14976        {
14977            self.advance();
14978            true
14979        } else {
14980            false
14981        };
14982        // Consume FULLTEXT / SPATIAL prefix and record which one
14983        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14984        // dedicated TableConstraint variant so the engine can
14985        // build a tsvector-GIN; SPATIAL still has no matching
14986        // AM, so it falls back to accept-as-no-op.
14987        let mut is_fulltext = false;
14988        let mut is_spatial = false;
14989        if let Token::Ident(s) = self.peek().clone() {
14990            if s.eq_ignore_ascii_case("fulltext") {
14991                self.advance();
14992                is_fulltext = true;
14993            } else if s.eq_ignore_ascii_case("spatial") {
14994                self.advance();
14995                is_spatial = true;
14996            }
14997        }
14998        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14999        // (reserved); accept either token shape.
15000        match self.peek() {
15001            Token::Index => {
15002                self.advance();
15003            }
15004            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15005                self.advance();
15006            }
15007            other => {
15008                return Err(self.err(alloc::format!(
15009                    "expected KEY/INDEX in inline index declaration, got {other:?}"
15010                )));
15011            }
15012        }
15013        // Optional index name (an ident before the `(`).
15014        // v7.15.0 — capture the name when present so the engine
15015        // builds the secondary index under the user's chosen
15016        // name (matches mysqldump's `KEY idx_x (col)` shape).
15017        let mut idx_name: Option<String> = None;
15018        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15019            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15020        {
15021            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15022                idx_name = Some(s);
15023            }
15024        }
15025        // Optional `USING BTREE` / `USING HASH` (MySQL).
15026        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15027            self.advance();
15028            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15029                self.advance();
15030            }
15031        }
15032        // Required column list `(col [, col]*)`.
15033        if !matches!(self.peek(), Token::LParen) {
15034            return Err(self.err(alloc::format!(
15035                "expected '(' in inline KEY/INDEX, got {:?}",
15036                self.peek()
15037            )));
15038        }
15039        self.advance();
15040        let mut cols: Vec<String> = Vec::new();
15041        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15042            self.advance();
15043            cols.push(s);
15044            // Skip optional `(length)` per-column prefix.
15045            if matches!(self.peek(), Token::LParen) {
15046                let mut depth = 1usize;
15047                self.advance();
15048                while depth > 0 {
15049                    match self.peek() {
15050                        Token::LParen => depth += 1,
15051                        Token::RParen => depth -= 1,
15052                        Token::Eof => break,
15053                        _ => {}
15054                    }
15055                    self.advance();
15056                }
15057            }
15058            // Skip optional ASC / DESC.
15059            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15060                || matches!(self.peek(), Token::Asc | Token::Desc)
15061            {
15062                self.advance();
15063            }
15064            if matches!(self.peek(), Token::Comma) {
15065                self.advance();
15066                continue;
15067            }
15068            break;
15069        }
15070        if matches!(self.peek(), Token::RParen) {
15071            self.advance();
15072        }
15073        // Trailing options on the inline index — comment / etc.
15074        // Skip until comma or `)`.
15075        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15076            self.advance();
15077        }
15078        if cols.is_empty() {
15079            return Ok(None);
15080        }
15081        if is_unique {
15082            // Carry the captured idx_name on UNIQUE too so future
15083            // engine work can name the underlying BTree
15084            // accordingly; today the unique-constraint installer
15085            // synthesises the name itself, but Display round-trip
15086            // benefits from preserving it.
15087            Ok(Some(crate::ast::TableConstraint::Unique {
15088                name: idx_name,
15089                columns: cols,
15090                nulls_not_distinct: false,
15091                // MySQL inline UNIQUE KEY has no deferral vocabulary.
15092                deferrable: false,
15093                initially_deferred: false,
15094            }))
15095        } else if is_fulltext {
15096            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15097            // routes through `TableConstraint::FulltextIndex`;
15098            // the engine builds a tsvector-GIN over each named
15099            // column so MATCH AGAINST gets a real inverted
15100            // index instead of a silently-dropped declaration.
15101            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15102                name: idx_name,
15103                columns: cols,
15104            }))
15105        } else if is_spatial {
15106            // SPG has no native SPATIAL AM. Accept-as-no-op
15107            // (declaration is parsed, but no index is built).
15108            Ok(None)
15109        } else {
15110            // v7.15.0 — plain KEY / INDEX builds a real BTree
15111            // secondary index.
15112            Ok(Some(crate::ast::TableConstraint::Index {
15113                name: idx_name,
15114                columns: cols,
15115            }))
15116        }
15117    }
15118
15119    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15120    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15121    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15122    /// (in any order, separated by whitespace).
15123    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15124    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15125    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15126    /// bare ident here, and only the parenthesised form is reloptions (so this
15127    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15128    fn consume_with_reloptions(&mut self) {
15129        let is_with = matches!(
15130            self.peek(),
15131            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15132        );
15133        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15134            return;
15135        }
15136        self.advance(); // WITH
15137        self.advance(); // (
15138        let mut depth = 1u32;
15139        while depth > 0 && !matches!(self.peek(), Token::Eof) {
15140            match self.peek() {
15141                Token::LParen => depth += 1,
15142                Token::RParen => depth -= 1,
15143                _ => {}
15144            }
15145            self.advance();
15146        }
15147    }
15148
15149    fn consume_mysql_table_options(&mut self) {
15150        loop {
15151            // Heuristic: a table option is an ident (or `DEFAULT`
15152            // reserved keyword) followed by `=` and an
15153            // ident / string / integer.
15154            let name_lc = match self.peek().clone() {
15155                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15156                Token::Default => alloc::string::String::from("default"),
15157                _ => break,
15158            };
15159            let known = matches!(
15160                name_lc.as_str(),
15161                "engine"
15162                    | "default"
15163                    | "charset"
15164                    | "collate"
15165                    | "auto_increment"
15166                    | "row_format"
15167                    | "comment"
15168                    | "pack_keys"
15169                    | "stats_persistent"
15170                    | "stats_auto_recalc"
15171                    | "stats_sample_pages"
15172                    | "key_block_size"
15173                    | "tablespace"
15174                    | "min_rows"
15175                    | "max_rows"
15176                    | "checksum"
15177                    | "delay_key_write"
15178                    | "insert_method"
15179                    | "data"
15180                    | "index"
15181                    | "encryption"
15182                    | "compression"
15183            );
15184            if !known {
15185                break;
15186            }
15187            self.advance(); // option name
15188            // `DEFAULT` optional prefix is followed by `CHARSET` /
15189            // `COLLATE`; consume the next ident too.
15190            if name_lc == "default" {
15191                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15192                    self.advance();
15193                }
15194            }
15195            if matches!(self.peek(), Token::Eq) {
15196                self.advance();
15197            }
15198            match self.peek() {
15199                Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
15200                    self.advance();
15201                }
15202                _ => {}
15203            }
15204        }
15205    }
15206
15207    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15208    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15209    /// sure (otherwise a column literally named `primary` would
15210    /// be mistaken).
15211    fn peek_table_level_pk_start(&self) -> bool {
15212        let cur = self.peek();
15213        let nxt = self.tokens.get(self.pos + 1);
15214        let nxt2 = self.tokens.get(self.pos + 2);
15215        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15216        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15217        let is_lparen = matches!(nxt2, Some(Token::LParen));
15218        is_primary && is_key && is_lparen
15219    }
15220
15221    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15222    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15223    /// (mailrs round-5 G10).
15224    fn peek_table_level_unique_start(&self) -> bool {
15225        let cur = self.peek();
15226        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15227        if !is_unique {
15228            return false;
15229        }
15230        let n1 = self.tokens.get(self.pos + 1);
15231        // Plain `UNIQUE (…)`.
15232        if matches!(n1, Some(Token::LParen)) {
15233            return true;
15234        }
15235        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15236        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15237        if !is_nulls {
15238            return false;
15239        }
15240        let n2 = self.tokens.get(self.pos + 2);
15241        let n3 = self.tokens.get(self.pos + 3);
15242        let n4 = self.tokens.get(self.pos + 4);
15243        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15244        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15245            return true;
15246        }
15247        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15248        if matches!(n2, Some(Token::Not))
15249            && matches!(n3, Some(Token::Distinct))
15250            && matches!(n4, Some(Token::LParen))
15251        {
15252            return true;
15253        }
15254        false
15255    }
15256
15257    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15258        self.advance(); // PRIMARY
15259        self.advance(); // KEY
15260        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15261        // v7.39 (round 711) — the trailer's values are CARRIED now; round
15262        // 621 consumed and dropped them (the storing half of F08).
15263        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15264        Ok(crate::ast::TableConstraint::PrimaryKey {
15265            name: None,
15266            columns,
15267            deferrable,
15268            initially_deferred,
15269        })
15270    }
15271
15272    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15273        self.advance(); // UNIQUE
15274        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15275        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15276        // is `NULLS DISTINCT` per the SQL standard.
15277        let mut nulls_not_distinct = false;
15278        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15279            let n1 = self.tokens.get(self.pos + 1);
15280            let n2 = self.tokens.get(self.pos + 2);
15281            let is_not = matches!(n1, Some(Token::Not));
15282            let is_distinct = matches!(n2, Some(Token::Distinct));
15283            if is_not && is_distinct {
15284                self.advance(); // NULLS
15285                self.advance(); // NOT
15286                self.advance(); // DISTINCT
15287                nulls_not_distinct = true;
15288            } else if matches!(n1, Some(Token::Distinct)) {
15289                self.advance(); // NULLS
15290                self.advance(); // DISTINCT
15291            }
15292        }
15293        let columns = self.parse_paren_ident_list("UNIQUE")?;
15294        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15295        Ok(crate::ast::TableConstraint::Unique {
15296            name: None,
15297            columns,
15298            nulls_not_distinct,
15299            deferrable,
15300            initially_deferred,
15301        })
15302    }
15303
15304    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15305    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15306    /// expression.
15307    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15308    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15309    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15310    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15311    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15312    /// commit: `NOT` starts no other suffix here, but reading both
15313    /// tokens before advancing keeps the caller's error message intact
15314    /// if someone writes `NOT NULL` by mistake.
15315    fn parse_not_valid_suffix(&mut self) -> bool {
15316        if !matches!(self.peek(), Token::Not) {
15317            return false;
15318        }
15319        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15320        {
15321            return false;
15322        }
15323        self.advance();
15324        self.advance();
15325        true
15326    }
15327
15328    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15329        self.advance(); // EXCLUDE
15330        // Optional `USING <method>`.
15331        let mut method = None;
15332        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15333            self.advance();
15334            method = Some(match self.advance() {
15335                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15336                other => {
15337                    return Err(self.err(alloc::format!(
15338                        "expected index method after USING, got {other:?}"
15339                    )));
15340                }
15341            });
15342        }
15343        if !matches!(self.peek(), Token::LParen) {
15344            return Err(self.err(alloc::format!(
15345                "expected '(' after EXCLUDE, got {:?}",
15346                self.peek()
15347            )));
15348        }
15349        self.advance();
15350        let mut elements: Vec<(String, String)> = Vec::new();
15351        loop {
15352            let col = match self.advance() {
15353                Token::Ident(s) | Token::QuotedIdent(s) => s,
15354                other => {
15355                    return Err(self.err(alloc::format!(
15356                        "expected column name in EXCLUDE, got {other:?}"
15357                    )));
15358                }
15359            };
15360            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15361                return Err(self.err(alloc::format!(
15362                    "expected WITH after EXCLUDE column, got {:?}",
15363                    self.peek()
15364                )));
15365            }
15366            self.advance();
15367            let op = match self.advance() {
15368                Token::InetOverlap => String::from("&&"),
15369                Token::Intersects => String::from("?#"),
15370                Token::IsBelow => String::from("<^"),
15371                Token::IsAbove => String::from(">^"),
15372                Token::PatternLt => String::from("~<~"),
15373                Token::PatternLtEq => String::from("~<=~"),
15374                Token::PatternGt => String::from("~>~"),
15375                Token::PatternGtEq => String::from("~>=~"),
15376                Token::TsMatchOld => String::from("@@@"),
15377                Token::Eq => String::from("="),
15378                Token::JsonContains => String::from("@>"),
15379                Token::JsonContainedBy => String::from("<@"),
15380                Token::OverLeft => String::from("&<"),
15381                Token::OverRight => String::from("&>"),
15382                other => {
15383                    return Err(self.err(alloc::format!(
15384                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15385                    )));
15386                }
15387            };
15388            elements.push((col, op));
15389            if matches!(self.peek(), Token::Comma) {
15390                self.advance();
15391                continue;
15392            }
15393            break;
15394        }
15395        if !matches!(self.peek(), Token::RParen) {
15396            return Err(self.err(alloc::format!(
15397                "expected ')' to close EXCLUDE, got {:?}",
15398                self.peek()
15399            )));
15400        }
15401        self.advance();
15402        Ok(crate::ast::TableConstraint::Exclude {
15403            name: None,
15404            method,
15405            elements,
15406        })
15407    }
15408
15409    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15410        self.advance(); // CHECK
15411        if !matches!(self.peek(), Token::LParen) {
15412            return Err(self.err(alloc::format!(
15413                "expected '(' after CHECK, got {:?}",
15414                self.peek()
15415            )));
15416        }
15417        self.advance();
15418        let expr = self.parse_expr(0)?;
15419        if !matches!(self.peek(), Token::RParen) {
15420            return Err(self.err(alloc::format!(
15421                "expected ')' to close CHECK predicate, got {:?}",
15422                self.peek()
15423            )));
15424        }
15425        self.advance();
15426        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15427        // are no existing rows for PG to skip, so it rejects the suffix.
15428        Ok(crate::ast::TableConstraint::Check {
15429            name: None,
15430            expr,
15431            not_valid: false,
15432        })
15433    }
15434
15435    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15436    fn peek_table_level_check_start(&self) -> bool {
15437        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15438    }
15439
15440    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15441    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15442    /// on the dedicated FK path (`parse_table_level_fk` consumes its
15443    /// own CONSTRAINT prefix).
15444    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15445        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15446            return None;
15447        }
15448        // tokens[pos+1] is the constraint name (any ident-like);
15449        // tokens[pos+2] is the kind keyword.
15450        match self.tokens.get(self.pos + 2) {
15451            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15452                Some(NamedTableConstraintKind::Check)
15453            }
15454            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15455                Some(NamedTableConstraintKind::Unique)
15456            }
15457            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15458                Some(NamedTableConstraintKind::PrimaryKey)
15459            }
15460            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15461                Some(NamedTableConstraintKind::Exclude)
15462            }
15463            _ => None,
15464        }
15465    }
15466
15467    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15468        if !matches!(self.peek(), Token::LParen) {
15469            return Err(self.err(alloc::format!(
15470                "expected '(' after {ctx}, got {:?}",
15471                self.peek()
15472            )));
15473        }
15474        self.advance();
15475        let mut out = Vec::new();
15476        loop {
15477            out.push(self.expect_ident_like()?);
15478            match self.peek() {
15479                Token::Comma => {
15480                    self.advance();
15481                }
15482                Token::RParen => {
15483                    self.advance();
15484                    break;
15485                }
15486                other => {
15487                    return Err(self.err(alloc::format!(
15488                        "expected ',' or ')' in {ctx} list, got {other:?}"
15489                    )));
15490                }
15491            }
15492        }
15493        if out.is_empty() {
15494            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15495        }
15496        Ok(out)
15497    }
15498
15499    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15500    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15501    /// table-level FK; a column def never starts with either keyword
15502    /// (column names are not in this reserved set).
15503    fn peek_constraint_or_fk_start(&self) -> bool {
15504        let is_constraint_kw = matches!(
15505            self.peek(),
15506            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15507        );
15508        let is_foreign_kw = matches!(
15509            self.peek(),
15510            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15511        );
15512        is_constraint_kw || is_foreign_kw
15513    }
15514
15515    /// v7.6.0 — parse a table-level FK clause:
15516    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15517    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15518    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15519        let mut name: Option<String> = None;
15520        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15521            self.advance();
15522            name = Some(self.expect_ident_like()?);
15523        }
15524        // `FOREIGN`
15525        match self.advance() {
15526            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15527            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15528        }
15529        // `KEY`
15530        match self.advance() {
15531            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15532            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15533        }
15534        // `(col, col, ...)`
15535        if !matches!(self.peek(), Token::LParen) {
15536            return Err(self.err(format!(
15537                "expected '(' after FOREIGN KEY, got {:?}",
15538                self.peek()
15539            )));
15540        }
15541        self.advance();
15542        let mut columns = Vec::new();
15543        loop {
15544            columns.push(self.expect_ident_like()?);
15545            match self.peek() {
15546                Token::Comma => {
15547                    self.advance();
15548                }
15549                Token::RParen => {
15550                    self.advance();
15551                    break;
15552                }
15553                other => {
15554                    return Err(self.err(format!(
15555                        "expected ',' or ')' in FK column list, got {other:?}"
15556                    )));
15557                }
15558            }
15559        }
15560        if columns.is_empty() {
15561            return Err(self.err("FOREIGN KEY requires at least one column".into()));
15562        }
15563        let (
15564            parent_table,
15565            parent_columns,
15566            on_delete,
15567            on_update,
15568            match_type,
15569            deferrable,
15570            initially_deferred,
15571        ) = self.parse_references_tail(columns.len())?;
15572        Ok(ForeignKeyConstraint {
15573            name,
15574            columns,
15575            parent_table,
15576            parent_columns,
15577            on_delete,
15578            on_update,
15579            match_type,
15580            deferrable,
15581            initially_deferred,
15582        })
15583    }
15584
15585    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15586    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15587    /// the local column count, used to default the parent column
15588    /// list when omitted (SQL spec: parent's PK is implied).
15589    fn parse_references_tail(
15590        &mut self,
15591        expected_arity: usize,
15592    ) -> Result<
15593        (
15594            String,
15595            Vec<String>,
15596            FkAction,
15597            FkAction,
15598            crate::ast::MatchType,
15599            // v7.39 (round 288) — deferrable, initially_deferred.
15600            bool,
15601            bool,
15602        ),
15603        ParseError,
15604    > {
15605        match self.advance() {
15606            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15607            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15608        }
15609        let parent_table = self.expect_ident_like()?;
15610        let mut parent_columns: Vec<String> = Vec::new();
15611        if matches!(self.peek(), Token::LParen) {
15612            self.advance();
15613            loop {
15614                parent_columns.push(self.expect_ident_like()?);
15615                match self.peek() {
15616                    Token::Comma => {
15617                        self.advance();
15618                    }
15619                    Token::RParen => {
15620                        self.advance();
15621                        break;
15622                    }
15623                    other => {
15624                        return Err(self.err(format!(
15625                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
15626                        )));
15627                    }
15628                }
15629            }
15630        }
15631        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15632            return Err(self.err(format!(
15633                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15634                expected_arity,
15635                parent_columns.len()
15636            )));
15637        }
15638        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15639        // it between the referenced column list and the ON / DEFERRABLE
15640        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15641        // is skipped when any referencing column is NULL), so SIMPLE —
15642        // the default, and the only spelling pg_dump emits — is accepted
15643        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15644        // mixed-NULL rule, which is not wired yet; reject them honestly
15645        // rather than silently applying SIMPLE (PG itself errors on
15646        // MATCH PARTIAL as "not yet implemented").
15647        let mut match_type = crate::ast::MatchType::Simple;
15648        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15649            self.advance();
15650            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15651            // SIMPLE / PARTIAL arrive as bare identifiers.
15652            let kind = match self.advance() {
15653                Token::Full => "FULL".to_string(),
15654                Token::Ident(s) => s.to_uppercase(),
15655                other => {
15656                    return Err(self.err(format!(
15657                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15658                    )));
15659                }
15660            };
15661            match kind.as_str() {
15662                "SIMPLE" => {} // Default — match_type stays Simple.
15663                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15664                // when ALL referencing columns are NULL; a mixed-NULL key errors.
15665                "FULL" => match_type = crate::ast::MatchType::Full,
15666                "PARTIAL" => {
15667                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15668                }
15669                _ => {
15670                    return Err(self.err(format!(
15671                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15672                    )));
15673                }
15674            }
15675        }
15676        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15677        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15678        // <action>` / `ON UPDATE <action>` in either order. PG /
15679        // pg_dump emits the timing clause AFTER the ON clauses
15680        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15681        // but the SQL spec allows either order. We loop over
15682        // every possible trailer and dispatch on the next token,
15683        // stopping when nothing matches. Phase 3.1 changes the
15684        // bare DEFERRABLE form from hard-error to accept-as-
15685        // immediate; SPG is single-writer with no deferred-
15686        // constraint window so the runtime semantics are always
15687        // immediate even when INITIALLY DEFERRED is requested.
15688        // PG's default referential action (no ON DELETE / ON UPDATE
15689        // clause) is NO ACTION, not RESTRICT — the two enforce
15690        // identically in SPG (single-writer, no deferred window; see the
15691        // shared match arm in constraints.rs) but information_schema.
15692        // referential_constraints must report NO ACTION to match PG.
15693        let mut on_delete = FkAction::NoAction;
15694        let mut on_update = FkAction::NoAction;
15695        let mut seen_on_delete = false;
15696        let mut seen_on_update = false;
15697        let mut deferrable = false;
15698        let mut initially_deferred = false;
15699        loop {
15700            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15701            let before = self.pos;
15702            let (d, idef) = self.consume_deferrable_clauses_timed()?;
15703            if self.pos != before {
15704                deferrable = d;
15705                initially_deferred = idef;
15706                continue;
15707            }
15708            // ON DELETE / ON UPDATE.
15709            if !matches!(self.peek(), Token::On) {
15710                break;
15711            }
15712            self.advance();
15713            let which = self.advance();
15714            let action = self.parse_fk_action()?;
15715            match which {
15716                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15717                    if seen_on_delete {
15718                        return Err(self.err("ON DELETE specified twice".into()));
15719                    }
15720                    seen_on_delete = true;
15721                    on_delete = action;
15722                }
15723                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15724                    if seen_on_update {
15725                        return Err(self.err("ON UPDATE specified twice".into()));
15726                    }
15727                    seen_on_update = true;
15728                    on_update = action;
15729                }
15730                other => {
15731                    return Err(
15732                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15733                    );
15734                }
15735            }
15736        }
15737        Ok((
15738            parent_table,
15739            parent_columns,
15740            on_delete,
15741            on_update,
15742            match_type,
15743            deferrable,
15744            initially_deferred,
15745        ))
15746    }
15747
15748    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15749    /// NO ACTION`.
15750    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15751        match self.advance() {
15752            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15753            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15754            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15755                Token::Null => Ok(FkAction::SetNull),
15756                Token::Default => Ok(FkAction::SetDefault),
15757                other => Err(self.err(format!(
15758                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15759                ))),
15760            },
15761            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15762                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15763                other => Err(self.err(format!(
15764                    "expected ACTION after NO in FK action, got {other:?}"
15765                ))),
15766            },
15767            other => Err(self.err(format!(
15768                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15769            ))),
15770        }
15771    }
15772
15773    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15774    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15775    fn consume_if_not_exists(&mut self) -> bool {
15776        // `IF` arrives as a bare Ident (we don't reserve it because it
15777        // also appears mid-expression in PG, though we don't support
15778        // those forms yet).
15779        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15780        if !looks_like_if {
15781            return false;
15782        }
15783        // Peek one ahead before committing: only consume IF when it's
15784        // actually `IF NOT EXISTS`.
15785        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15786            return false;
15787        }
15788        if !matches!(
15789            self.tokens.get(self.pos + 2),
15790            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15791        ) {
15792            return false;
15793        }
15794        self.advance(); // IF
15795        self.advance(); // NOT
15796        self.advance(); // EXISTS
15797        true
15798    }
15799
15800    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15801    /// Consumes IF EXISTS as a pair; returns false otherwise
15802    /// without consuming any tokens.
15803    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15804    /// ENABLE/DISABLE/FORCE/NO FORCE.
15805    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15806        for kw in ["row", "level", "security"] {
15807            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15808            {
15809                return Err(self.err(alloc::format!(
15810                    "expected {} in ROW LEVEL SECURITY, got {:?}",
15811                    kw.to_ascii_uppercase(),
15812                    self.peek()
15813                )));
15814            }
15815            self.advance();
15816        }
15817        Ok(())
15818    }
15819
15820    fn consume_if_exists(&mut self) -> bool {
15821        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15822        if !looks_like_if {
15823            return false;
15824        }
15825        if !matches!(
15826            self.tokens.get(self.pos + 1),
15827            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15828        ) {
15829            return false;
15830        }
15831        self.advance(); // IF
15832        self.advance(); // EXISTS
15833        true
15834    }
15835
15836    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15837    /// qualifiers after an index column ref. ASC / DESC are
15838    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15839    /// We accept and discard them since single-column BTree
15840    /// stores rows in natural key order today.
15841    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15842    /// ORDER BY key. Returns None when absent.
15843    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15844        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15845            return Ok(None);
15846        }
15847        self.advance();
15848        match self.advance() {
15849            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15850            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15851            other => Err(self.err(alloc::format!(
15852                "expected FIRST or LAST after NULLS, got {other:?}"
15853            ))),
15854        }
15855    }
15856
15857    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15858    /// rather than discarded.
15859    ///
15860    /// SPG's index does not scan in a direction — column ordering is
15861    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15862    /// reproduction of the DDL, and dropping the clause meant
15863    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15864    /// dump lost it, and a schema diff saw drift on every run.
15865    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15866        let mut order = crate::ast::IndexColumnOrder::default();
15867        loop {
15868            match self.peek() {
15869                Token::Asc => {
15870                    self.advance();
15871                }
15872                Token::Desc => {
15873                    order.descending = true;
15874                    self.advance();
15875                }
15876                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15877                    let look = self.tokens.get(self.pos + 1);
15878                    if matches!(
15879                        look,
15880                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15881                            || k.eq_ignore_ascii_case("last")
15882                    ) {
15883                        self.advance();
15884                        order.nulls_first = Some(matches!(
15885                            self.advance(),
15886                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
15887                        ));
15888                    } else {
15889                        break;
15890                    }
15891                }
15892                _ => break,
15893            }
15894        }
15895        order
15896    }
15897
15898    fn parse_create_index_stmt_after_create(
15899        &mut self,
15900        is_unique: bool,
15901    ) -> Result<Statement, ParseError> {
15902        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15903        debug_assert!(matches!(self.peek(), Token::Index));
15904        self.advance();
15905        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15906        // SPG's CREATE INDEX is synchronous end-to-end today (real
15907        // CONCURRENTLY variant with restartable scans queues with
15908        // v7.39 indexes epic), so the modifier has no runtime effect
15909        // — same accept-and-no-op shape as v7.37.16.5 DETACH
15910        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15911        // VIEW CONCURRENTLY.
15912        let mut concurrently = false;
15913        if matches!(
15914            self.peek(),
15915            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15916        ) {
15917            self.advance();
15918            concurrently = true;
15919        }
15920        let if_not_exists = self.consume_if_not_exists();
15921        // v7.39 (read01 round 93) — the index name is optional (PG since
15922        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15923        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15924        // was given; leave it empty and the engine derives a PG-style
15925        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15926        let name = if matches!(self.peek(), Token::On) {
15927            String::new()
15928        } else {
15929            self.expect_ident_like()?
15930        };
15931        if !matches!(self.peek(), Token::On) {
15932            return Err(self.err(format!(
15933                "expected ON after CREATE INDEX <name>, got {:?}",
15934                self.peek()
15935            )));
15936        }
15937        self.advance();
15938        let table = self.expect_ident_like()?;
15939        // Optional `USING <method>` — only recognised method in v2.0 is
15940        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15941        // ident `using` (we don't promote it to a reserved keyword
15942        // because it isn't reserved anywhere else in our SQL surface).
15943        let mut method_name: Option<String> = None;
15944        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15945            self.advance();
15946            let m = self.expect_ident_like()?;
15947            method_name = Some(m.to_ascii_lowercase());
15948            match m.to_ascii_lowercase().as_str() {
15949                "hnsw" => IndexMethod::Hnsw,
15950                "btree" => IndexMethod::BTree,
15951                "brin" => IndexMethod::Brin,
15952                // v7.12.3 — real GIN inverted index over `tsvector`.
15953                // v7.9.26b's `USING gin` → BTree silent fallback is
15954                // gone; the engine validates that the indexed column
15955                // is `tsvector` at CREATE INDEX time.
15956                "gin" => IndexMethod::Gin,
15957                // v7.9.26b — PG `pg_dump` emits `USING gist` /
15958                // `USING spgist` / `USING hash` for their built-in
15959                // AMs that SPG doesn't have a matching
15960                // implementation for; degrade to BTree on the
15961                // leading column so the schema loads + the index
15962                // catalogue stays consistent. Operator pays the
15963                // planner cost only for the queries that would have
15964                // used the specialised AM.
15965                "gist" | "spgist" | "hash" => IndexMethod::BTree,
15966                // v7.11.3 — pgvector ships both `ivfflat` and
15967                // `hnsw`. Customers shouldn't have to choose
15968                // their on-disk index method based on what SPG
15969                // implements; accept `ivfflat` as a synonym for
15970                // `hnsw` so PG schemas using either method drop
15971                // in. The vector distance op (`<->` / `<#>` /
15972                // `<=>`) at query time still picks the metric.
15973                "ivfflat" => IndexMethod::Hnsw,
15974                other => {
15975                    return Err(self.err(alloc::format!(
15976                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15977                    )));
15978                }
15979            }
15980        } else {
15981            IndexMethod::BTree
15982        };
15983        if !matches!(self.peek(), Token::LParen) {
15984            return Err(self.err(format!(
15985                "expected '(' before indexed column, got {:?}",
15986                self.peek()
15987            )));
15988        }
15989        self.advance();
15990        // v6.8.2 — accept either a bare column ident (legacy) or
15991        // an expression `fn(col, …)` for expression indexes.
15992        // Distinguish by peeking the token *after* the current
15993        // ident: `ident )` is the legacy column-only path;
15994        // anything else triggers the Pratt expression parser.
15995        // (`advance()` uses `mem::replace` to nil out the current
15996        // slot, so we can't save+rewind cleanly — peek-ahead via
15997        // direct index avoids the mutation.)
15998        let mut opclass: Option<String> = None;
15999        let mut key_collation: Option<String> = None;
16000        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16001            // Single column with `)` immediately after — fast path.
16002            // v7.9.29 — also: bare column followed by `,` (the
16003            // multi-column form `(a, b, c)`). Without this branch
16004            // the leading ident gets pulled into `parse_expr`
16005            // which then sets `expression = Some(Column(a))` and
16006            // breaks Display round-trip on the multi-column shape.
16007            Token::Ident(s) | Token::QuotedIdent(s)
16008                if matches!(
16009                    self.tokens.get(self.pos + 1),
16010                    Some(Token::RParen | Token::Comma)
16011                ) =>
16012            {
16013                self.advance();
16014                (s, None)
16015            }
16016            // v7.9.22 — single column followed by a pgvector
16017            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16018            // v7.15.0 — capture the opclass instead of discarding
16019            // it so the engine can dispatch (e.g. `gin_trgm_ops`
16020            // → real trigram-shingle GIN over a TEXT column).
16021            // Vector/HNSW opclasses still take their distance
16022            // metric from the query operator (`<->` / `<#>` /
16023            // `<=>`), so for those callers the opclass stays
16024            // informational.
16025            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16026            // opclass: `(embedding public.vector_cosine_ops)`. Strip
16027            // the schema and dispatch on the bare opclass, the same
16028            // treatment table/type names get.
16029            Token::Ident(s) | Token::QuotedIdent(s)
16030                if matches!(
16031                    self.tokens.get(self.pos + 1),
16032                    Some(Token::Ident(_) | Token::QuotedIdent(_))
16033                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16034                    && matches!(
16035                        self.tokens.get(self.pos + 3),
16036                        Some(Token::Ident(op) | Token::QuotedIdent(op))
16037                            if is_vector_opclass_name(op)
16038                    ) =>
16039            {
16040                self.advance(); // column name
16041                self.advance(); // schema qualifier
16042                self.advance(); // dot
16043                let op_tok = self.advance();
16044                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16045                    opclass = Some(op.to_ascii_lowercase());
16046                }
16047                (s, None)
16048            }
16049            // r1038 — an operator class is recognised by its POSITION, not
16050            // by a list of names. It used to be `is_vector_opclass_name`,
16051            // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16052            // sentori's migration wrote — was a syntax error while
16053            // `USING gin (doc)` parsed. Anything sitting between a column
16054            // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16055            // two bare identifiers in a row are not valid there otherwise.
16056            Token::Ident(s) | Token::QuotedIdent(s)
16057                if matches!(
16058                    self.tokens.get(self.pos + 1),
16059                    Some(Token::Ident(op) | Token::QuotedIdent(op))
16060                        if is_vector_opclass_name(op) || Self::opclass_position_follows(
16061                            self.tokens.get(self.pos + 2)
16062                        )
16063                ) =>
16064            {
16065                self.advance(); // column name
16066                // Capture the opclass token, lower-cased for
16067                // case-insensitive engine dispatch.
16068                let op_tok = self.advance();
16069                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16070                    opclass = Some(op.to_ascii_lowercase());
16071                }
16072                (s, None)
16073            }
16074            Token::Ident(_) | Token::QuotedIdent(_) => {
16075                // v7.39 (round 538) — an explicit COLLATE on the key,
16076                // read by LOOKAHEAD because `parse_expr` absorbs the
16077                // clause as a no-op (SPG orders text by bytes, which is
16078                // the C collation, so it changes nothing to honour). PG
16079                // still PRINTS it: an explicitly written `"C"` and the
16080                // collation a column inherits are different collation
16081                // OBJECTS even where they sort identically, which is why
16082                // `(a COLLATE "C")` shows on a C-collation database too.
16083                if matches!(
16084                    self.tokens.get(self.pos + 1),
16085                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16086                ) {
16087                    key_collation = match self.tokens.get(self.pos + 2) {
16088                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16089                            Some(n.clone())
16090                        }
16091                        _ => None,
16092                    };
16093                }
16094                let key_expr = self.parse_expr(0)?;
16095                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16096                    self.err("expression index key must reference at least one column".into())
16097                })?;
16098                (primary, Some(key_expr))
16099            }
16100            // v7.37.43-T4 — parenthesised expression index key
16101            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16102            // PG's CREATE INDEX requires the expression to be in
16103            // its own parens to disambiguate function calls from
16104            // column lists, so this `LParen` is the inner open-paren
16105            // of an expression key. parse_expr handles the recursive
16106            // descent and consumes the matching `RParen`.
16107            Token::LParen => {
16108                let key_expr = self.parse_expr(0)?;
16109                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16110                    self.err("expression index key must reference at least one column".into())
16111                })?;
16112                (primary, Some(key_expr))
16113            }
16114            other => {
16115                return Err(self.err(format!(
16116                    "expected column ident or expression, got {other:?}"
16117                )));
16118            }
16119        };
16120        // v7.9.14 — accept extra comma-separated columns inside
16121        // the index key parens (`CREATE INDEX … (a, b, c)`).
16122        // mailrs F2. Each extra column may carry an optional
16123        // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
16124        // — parsed and discarded; SPG doesn't honour direction
16125        // on a BTree index today (column ordering is intrinsic
16126        // to the storage). v7.10 will widen to genuine composite
16127        // index keys.
16128        let mut extra_columns: Vec<String> = Vec::new();
16129        // The leading column may also have ASC/DESC after it — and that
16130        // one is the column SPG indexes, so its clause is kept.
16131        let key_order = self.consume_optional_index_column_qualifiers();
16132        while matches!(self.peek(), Token::Comma) {
16133            self.advance();
16134            let extra = self.expect_ident_like()?;
16135            let _ = self.consume_optional_index_column_qualifiers();
16136            extra_columns.push(extra);
16137        }
16138        if !matches!(self.peek(), Token::RParen) {
16139            return Err(self.err(format!(
16140                "expected ')' after indexed column / expression, got {:?}",
16141                self.peek()
16142            )));
16143        }
16144        self.advance();
16145        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16146        // index-only-scan annotation. Bare ident (not a reserved
16147        // keyword) so we test by case-insensitive string match.
16148        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16149        {
16150            self.advance();
16151            if !matches!(self.peek(), Token::LParen) {
16152                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16153            }
16154            self.advance();
16155            let mut cols = Vec::new();
16156            loop {
16157                cols.push(self.expect_ident_like()?);
16158                match self.peek() {
16159                    Token::Comma => {
16160                        self.advance();
16161                    }
16162                    Token::RParen => {
16163                        self.advance();
16164                        break;
16165                    }
16166                    other => {
16167                        return Err(self.err(format!(
16168                            "expected ',' or ')' in INCLUDE list, got {other:?}"
16169                        )));
16170                    }
16171                }
16172            }
16173            cols
16174        } else {
16175            Vec::new()
16176        };
16177        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16178        // storage parameters. pgvector emits `WITH (lists = N)` for
16179        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16180        // SPG's HNSW picks its own parameters today (tunable via
16181        // env vars), so the WITH clause is informational and dropped.
16182        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16183            self.advance();
16184            if !matches!(self.peek(), Token::LParen) {
16185                return Err(self.err(format!(
16186                    "expected '(' after WITH in CREATE INDEX, got {:?}",
16187                    self.peek()
16188                )));
16189            }
16190            self.advance();
16191            loop {
16192                if matches!(self.peek(), Token::RParen) {
16193                    self.advance();
16194                    break;
16195                }
16196                // Drain `key = value` or bare `key` tokens.
16197                let _ = self.advance(); // key
16198                if matches!(self.peek(), Token::Eq) {
16199                    self.advance();
16200                    let _ = self.advance(); // value (int / string / ident)
16201                }
16202                match self.peek() {
16203                    Token::Comma => {
16204                        self.advance();
16205                    }
16206                    Token::RParen => {
16207                        self.advance();
16208                        break;
16209                    }
16210                    other => {
16211                        return Err(self.err(format!(
16212                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
16213                        )));
16214                    }
16215                }
16216            }
16217        }
16218        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16219        // which sits between the key list and the WHERE clause.
16220        let mut nulls_not_distinct = false;
16221        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16222            let n1 = self.tokens.get(self.pos + 1);
16223            let n2 = self.tokens.get(self.pos + 2);
16224            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16225                self.advance(); // NULLS
16226                self.advance(); // NOT
16227                self.advance(); // DISTINCT
16228                nulls_not_distinct = true;
16229            } else if matches!(n1, Some(Token::Distinct)) {
16230                self.advance(); // NULLS
16231                self.advance(); // DISTINCT
16232            }
16233        }
16234        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16235        let partial_predicate = if matches!(self.peek(), Token::Where) {
16236            self.advance();
16237            Some(self.parse_expr(0)?)
16238        } else {
16239            None
16240        };
16241        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16242        // sense: uniqueness over an ANN structure has no clean
16243        // semantics. Reject early. (BRIN UNIQUE is similarly
16244        // meaningless — block both.)
16245        if is_unique && !matches!(method, IndexMethod::BTree) {
16246            return Err(self.err(alloc::format!(
16247                "UNIQUE is only supported on BTree indexes, got USING {:?}",
16248                method
16249            )));
16250        }
16251        Ok(Statement::CreateIndex(CreateIndexStatement {
16252            concurrently,
16253            name,
16254            key_order,
16255            key_collation,
16256            table,
16257            column,
16258            nulls_not_distinct,
16259            method,
16260            if_not_exists,
16261            included_columns,
16262            partial_predicate,
16263            extra_columns: extra_columns.clone(),
16264            expression,
16265            is_unique,
16266            opclass,
16267            method_name,
16268        }))
16269    }
16270
16271    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16272    /// column-level `REFERENCES ...` clause. The trailing FK is
16273    /// normalised into table-level shape (single-element columns +
16274    /// parent_columns) so the engine sees one uniform constraint list.
16275    fn parse_column_def_with_fk(
16276        &mut self,
16277    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16278        let col = self.parse_column_def()?;
16279        // v7.39 (round 308, V29) — an explicitly named inline FK:
16280        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16281        // loop leaves this spelling intact precisely so the name can be
16282        // kept here; PG reports it in violation messages and matches it
16283        // in `SET CONSTRAINTS`.
16284        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16285        {
16286            self.advance();
16287            Some(self.expect_ident_like()?)
16288        } else {
16289            None
16290        };
16291        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16292        let inline_references = matches!(
16293            self.peek(),
16294            Token::Ident(s) if s.eq_ignore_ascii_case("references")
16295        );
16296        if !inline_references {
16297            return Ok((col, None));
16298        }
16299        let (
16300            parent_table,
16301            parent_columns,
16302            on_delete,
16303            on_update,
16304            match_type,
16305            deferrable,
16306            initially_deferred,
16307        ) = self.parse_references_tail(1)?;
16308        let fk = ForeignKeyConstraint {
16309            name: declared_name,
16310            columns: vec![col.name.clone()],
16311            parent_table,
16312            parent_columns,
16313            on_delete,
16314            on_update,
16315            match_type,
16316            deferrable,
16317            initially_deferred,
16318        };
16319        Ok((col, Some(fk)))
16320    }
16321
16322    /// v7.13.0 — parse a column type (consuming the type ident and
16323    /// any trailing parameters / `[]`), without surrounding column
16324    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16325    /// Returns the resolved `ColumnTypeName` plus implied
16326    /// `(auto_increment, not_null)` flags from PG SERIAL family
16327    /// shorthands — callers that don't expect those (ALTER COLUMN
16328    /// TYPE) can discard them.
16329    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16330        let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16331        Ok(ty)
16332    }
16333
16334    #[allow(clippy::type_complexity)]
16335    fn parse_type_with_implied_flags(
16336        &mut self,
16337    ) -> Result<
16338        (
16339            ColumnTypeName,
16340            bool,
16341            bool,
16342            Option<String>,
16343            Collation,
16344            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16345            bool,
16346            // v7.39 (round 676) — the collation NAME as written, which the
16347            // `Collation` enum above cannot carry.
16348            Option<String>,
16349            bool,
16350            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16351            // list captured at type-parse time. None for all
16352            // non-ENUM types.
16353            Option<Vec<String>>,
16354            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16355            // list. Distinct from ENUM (subset semantics).
16356            Option<Vec<String>>,
16357            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16358            // width, lost when the type collapses to SmallInt / Int.
16359            Option<MysqlIntWidth>,
16360            // v7.39 (round 424) — declared fractional-seconds precision of a
16361            // MySQL temporal column (bare spelling = 0). None under PG.
16362            Option<u8>,
16363        ),
16364        ParseError,
16365    > {
16366        let mut ty_ident = match self.advance() {
16367            Token::Ident(s) => s,
16368            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16369            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16370            // '<span>'` literal grammar. As a column type it lands
16371            // here directly; downstream resolution still uses the
16372            // canonical lowercase string.
16373            Token::Interval => "interval".to_string(),
16374            other => {
16375                return Err(ParseError {
16376                    message: format!("expected column type, got {other:?}"),
16377                    token_pos: self.consumed_pos(),
16378                });
16379            }
16380        };
16381        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16382        // pg_dump qualifies extension types (`public.vector(1024)`).
16383        // SPG is single-namespace; drop the schema and resolve the
16384        // bare type — same treatment table names already get.
16385        while matches!(self.peek(), Token::Dot) {
16386            self.advance();
16387            ty_ident = self.expect_ident_like()?;
16388        }
16389        let mut implied_auto_increment = false;
16390        let mut implied_not_null = false;
16391        let mut user_type_ref: Option<String> = None;
16392        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16393        // value list, captured here and bubbled up through the
16394        // ColumnDef so the engine can attach it to the column
16395        // schema (and validate INSERT cells against it).
16396        let mut inline_enum_variants: Option<Vec<String>> = None;
16397        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16398        let mut inline_set_variants: Option<Vec<String>> = None;
16399        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16400        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16401        // collapses to SmallInt / Int. Only under the MySQL dialect.
16402        let mut mysql_int_width: Option<MysqlIntWidth> = None;
16403        // v7.39 (round 424) — the declared fractional-seconds precision of a
16404        // MySQL temporal column. Set by the temporal arms below; stays None
16405        // for PG (whose temporal columns keep full microseconds).
16406        let mut mysql_fsp: Option<u8> = None;
16407        let mut ty = match ty_ident.as_str() {
16408            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16409            "smallserial" | "serial2" => {
16410                implied_auto_increment = true;
16411                implied_not_null = true;
16412                ColumnTypeName::SmallInt
16413            }
16414            "serial" | "serial4" => {
16415                implied_auto_increment = true;
16416                implied_not_null = true;
16417                ColumnTypeName::Int
16418            }
16419            "bigserial" | "serial8" => {
16420                implied_auto_increment = true;
16421                implied_not_null = true;
16422                ColumnTypeName::BigInt
16423            }
16424            // MySQL flavours we accept by aliasing to the closest SPG
16425            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16426            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16427            // 24-bit) → INT. UNSIGNED modifiers are consumed below
16428            // without semantic effect.
16429            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16430            // PG's internal type names; pg_dump and hand-written PG schemas
16431            // use them interchangeably with smallint / int / bigint (the cast
16432            // path already accepted them, only the column grammar didn't).
16433            "smallint" | "int2" => {
16434                // v7.14.0 — MySQL display-width on integers
16435                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16436                // parenthesised number is purely cosmetic — it
16437                // doesn't change storage. Accept + discard.
16438                self.consume_optional_paren_size();
16439                ColumnTypeName::SmallInt
16440            }
16441            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16442            // canonical encoding for BOOLEAN. Every MySQL driver
16443            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16444            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16445            // 4.3 SPG classified TINYINT(1) as SmallInt, which
16446            // gave the customer i16-shaped values where the app
16447            // expected bool — a Tier-A silent type drift on
16448            // mysqldump restores. Now: `TINYINT(1)` → Bool;
16449            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16450            // stay SmallInt (the legacy width-agnostic path).
16451            "tinyint" => {
16452                let width = self.peek_optional_paren_size_value();
16453                self.consume_optional_paren_size();
16454                if width == Some(1) {
16455                    ColumnTypeName::Bool
16456                } else {
16457                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16458                    // lost width so the write path can enforce -128..127.
16459                    if self.mysql_dialect {
16460                        mysql_int_width = Some(MysqlIntWidth::Tiny);
16461                    }
16462                    ColumnTypeName::SmallInt
16463                }
16464            }
16465            "mediumint" => {
16466                self.consume_optional_paren_size();
16467                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16468                if self.mysql_dialect {
16469                    mysql_int_width = Some(MysqlIntWidth::Medium);
16470                }
16471                ColumnTypeName::Int
16472            }
16473            "int" | "integer" | "int4" => {
16474                self.consume_optional_paren_size();
16475                ColumnTypeName::Int
16476            }
16477            "bigint" | "int8" => {
16478                self.consume_optional_paren_size();
16479                ColumnTypeName::BigInt
16480            }
16481            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16482            // (mailrs round-5 G6). Consume the optional `PRECISION`
16483            // tail when the type keyword was `double` / `DOUBLE`.
16484            //
16485            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16486            // FLOAT". `FLOAT(p)` picks the width the way PG does:
16487            // p in 1..=24 is real, 25..=53 is double precision, and
16488            // anything else is an error.
16489            "float" | "double" | "real" => {
16490                if ty_ident.eq_ignore_ascii_case("double")
16491                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16492                {
16493                    self.advance();
16494                }
16495                if ty_ident.eq_ignore_ascii_case("real") {
16496                    // v7.39 (round 274) — the two dialects genuinely
16497                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16498                    // synonym for DOUBLE (8-byte). Round 269 made REAL
16499                    // 32-bit globally and thereby narrowed the stored
16500                    // precision of every MySQL REAL column.
16501                    if self.mysql_dialect {
16502                        ColumnTypeName::Float
16503                    } else {
16504                        ColumnTypeName::Real
16505                    }
16506                } else if ty_ident.eq_ignore_ascii_case("float")
16507                    && self.mysql_dialect
16508                    && matches!(self.peek(), Token::LParen)
16509                    && self.peek_paren_has_comma()
16510                {
16511                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16512                    // display form (`FLOAT(10,2)`), which PG has no
16513                    // equivalent of. It was `syntax error at or near ","`,
16514                    // so the whole CREATE failed. The digits are a display
16515                    // hint only; SPG stores the full double.
16516                    self.consume_optional_paren_size();
16517                    ColumnTypeName::Float
16518                } else if ty_ident.eq_ignore_ascii_case("float")
16519                    && matches!(self.peek(), Token::LParen)
16520                {
16521                    // PG words the two bounds differently, and
16522                    // parse_paren_size already rejects a zero.
16523                    let p = self.parse_paren_size("FLOAT")?;
16524                    if p > 53 {
16525                        return Err(self.err(String::from(
16526                            "precision for type float must be less than 54 bits",
16527                        )));
16528                    }
16529                    if p <= 24 {
16530                        ColumnTypeName::Real
16531                    } else {
16532                        ColumnTypeName::Float
16533                    }
16534                } else {
16535                    ColumnTypeName::Float
16536                }
16537            }
16538            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16539            "float4" => ColumnTypeName::Real,
16540            "float8" => ColumnTypeName::Float,
16541            "text" => ColumnTypeName::Text,
16542            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16543            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16544            // real MySQL schema and NONE of them existed: the CREATE
16545            // failed outright with `type "blob" does not exist`, so the
16546            // table was never made. The sizes differ only in MySQL's
16547            // maximum length, which SPG does not cap, so they collapse
16548            // onto TEXT and BYTEA the way the unsized spellings do.
16549            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16550            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16551            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16552            // enforce, consumed so the declaration parses.
16553            "varbinary" | "binary" => {
16554                self.consume_optional_paren_size();
16555                ColumnTypeName::Bytes
16556            }
16557            "name" => ColumnTypeName::Name,
16558            "xid" => ColumnTypeName::Xid,
16559            "oid" => ColumnTypeName::Oid,
16560            "xid8" => ColumnTypeName::Xid8,
16561            "bool" | "boolean" => ColumnTypeName::Bool,
16562            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16563            // an unbounded `character varying`, which the arm below has always
16564            // read as text. Only the short spelling demanded a length, so
16565            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16566            // there is — failed on `VARCHAR type requires (N)` while the long
16567            // spelling of the same thing was accepted. The same asymmetry
16568            // round 613 closed on the CAST side, here on the DDL side.
16569            "varchar" => {
16570                if matches!(self.peek(), Token::LParen) {
16571                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16572                } else {
16573                    ColumnTypeName::Text
16574                }
16575            }
16576            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16577            // `character` below (SQL standard).
16578            "char" => {
16579                if matches!(self.peek(), Token::LParen) {
16580                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16581                } else {
16582                    ColumnTypeName::Char(1)
16583                }
16584            }
16585            // pg_dump's canonical spellings: `character varying(n)` = varchar,
16586            // `character(n)` = char, bare `character` = char(1). Unbounded
16587            // `character varying` maps to text.
16588            "character" => {
16589                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16590                    self.advance();
16591                    if matches!(self.peek(), Token::LParen) {
16592                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16593                    } else {
16594                        ColumnTypeName::Text
16595                    }
16596                } else if matches!(self.peek(), Token::LParen) {
16597                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16598                } else {
16599                    ColumnTypeName::Char(1)
16600                }
16601            }
16602            "vector" => {
16603                let dim = self.parse_paren_size("VECTOR")?;
16604                let encoding = self.parse_optional_vector_encoding()?;
16605                ColumnTypeName::Vector { dim, encoding }
16606            }
16607            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16608            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16609            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16610            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16611            // DECIMAL(10,2))` — how nearly every money column is written,
16612            // in either dialect — was a syntax error and the table was
16613            // never created. `FIXED` is MySQL's alias alone, so it is
16614            // taken only in that dialect.
16615            "numeric" | "decimal" | "dec" => {
16616                let (precision, scale) = self.parse_optional_numeric_params()?;
16617                ColumnTypeName::Numeric(precision, scale)
16618            }
16619            "fixed" if self.mysql_dialect => {
16620                let (precision, scale) = self.parse_optional_numeric_params()?;
16621                ColumnTypeName::Numeric(precision, scale)
16622            }
16623            "date" => ColumnTypeName::Date,
16624            // MySQL's `DATETIME` is the same domain as standard
16625            // `TIMESTAMP` — accept both spellings.
16626            "timestamp" | "datetime" => {
16627                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16628                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16629                // TIME ZONE` clause, so consume it first.
16630                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16631                // (it truncates on write and pads on render), so capture it;
16632                // a bare spelling means precision 0 there. PG stores µs always
16633                // and keeps `None`.
16634                let n = self.take_optional_paren_size();
16635                if self.mysql_dialect {
16636                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16637                }
16638                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16639                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16640                // the full form. SPG canonicalises:
16641                //   - WITH TIME ZONE    → Timestamptz
16642                //   - WITHOUT TIME ZONE → Timestamp
16643                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16644                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16645                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16646                {
16647                    self.advance(); // WITH
16648                    self.advance(); // TIME
16649                    self.advance(); // ZONE
16650                    ColumnTypeName::Timestamptz
16651                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16652                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16653                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16654                {
16655                    self.advance(); // WITHOUT
16656                    self.advance(); // TIME
16657                    self.advance(); // ZONE
16658                    ColumnTypeName::Timestamp
16659                } else {
16660                    // A second `(precision)` cannot legally follow, but the
16661                    // old grammar tolerated it; keep that tolerance.
16662                    self.consume_optional_paren_size();
16663                    ColumnTypeName::Timestamp
16664                }
16665            }
16666            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16667            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16668            // only PG-wire OID differs.
16669            "timestamptz" => {
16670                self.consume_optional_paren_size();
16671                ColumnTypeName::Timestamptz
16672            }
16673            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16674            // validation. We accept the JSONB spelling too because
16675            // most PG clients default to it; SPG doesn't distinguish
16676            // the two (no path-operator perf advantage to model).
16677            "json" => ColumnTypeName::Json,
16678            "jsonb" => ColumnTypeName::Jsonb,
16679            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16680            // surface here. Same storage shape; mapping happens at
16681            // the engine side via the ColumnTypeName → DataType
16682            // resolver. Literal forms are handled at coerce_value
16683            // time so the lexer stays untouched.
16684            "bytea" | "bytes" => ColumnTypeName::Bytes,
16685            // v7.17.0 Phase 7 — PG network address types
16686            // v7.17.0 had a Text-backed fallback here for
16687            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16688            // each to a first-class type; the keywords are
16689            // bound below in the ζ-A block.
16690            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16691            // The actual `to_tsvector` / `@@` / `ts_rank` surface
16692            // arrives in v7.12.1+; the type itself loads here so
16693            // mailrs's `scripts/init-schema.sql` runs unmodified.
16694            "tsvector" => ColumnTypeName::TsVector,
16695            "tsquery" => ColumnTypeName::TsQuery,
16696            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16697            // surface for Django / Rails / Hibernate's default
16698            // PK pattern.
16699            "uuid" => ColumnTypeName::Uuid,
16700            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16701            // Storage = three-field {months, days, micros}, catalog
16702            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16703            // line `INTERVAL` was parser-rejected at CREATE TABLE.
16704            "interval" => {
16705                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16706                // SECOND` and an optional `(p)` precision. SPG stores the full
16707                // {months,days,micros}; consume + ignore the qualifier/precision.
16708                while matches!(self.peek(), Token::To)
16709                    || matches!(self.peek(), Token::Ident(s) if matches!(
16710                        s.to_ascii_lowercase().as_str(),
16711                        "year" | "month" | "day" | "hour" | "minute" | "second"
16712                    ))
16713                {
16714                    self.advance();
16715                }
16716                self.consume_optional_paren_size();
16717                ColumnTypeName::Interval
16718            }
16719            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16720            // i64 microseconds since 00:00:00. Wire OID 1083.
16721            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16722            "time" => {
16723                // v7.39 (round 424) — MySQL TIME carries a semantic
16724                // fractional-seconds precision, bare meaning 0.
16725                let n = self.take_optional_paren_size();
16726                if self.mysql_dialect {
16727                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16728                }
16729                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16730                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16731                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16732                {
16733                    self.advance();
16734                    self.advance();
16735                    self.advance();
16736                    ColumnTypeName::TimeTz
16737                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16738                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16739                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16740                {
16741                    self.advance();
16742                    self.advance();
16743                    self.advance();
16744                    ColumnTypeName::Time
16745                } else {
16746                    ColumnTypeName::Time
16747                }
16748            }
16749            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16750            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16751            "year" => ColumnTypeName::Year,
16752            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16753            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16754            "timetz" => ColumnTypeName::TimeTz,
16755            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16756            // Wire OID 790.
16757            "money" => ColumnTypeName::Money,
16758            // v7.17.0 Phase 3.P0-38 — PG range types.
16759            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16760            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16761            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16762            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16763            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16764            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16765            // v7.37.5 δ — PG 14+ multirange keywords.
16766            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16767            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16768            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16769            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16770            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16771            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16772            // v7.37.5 ε — PG geometry scalar keywords.
16773            "point" => ColumnTypeName::Point,
16774            "lseg" => ColumnTypeName::Lseg,
16775            "path" => ColumnTypeName::Path,
16776            "box" => ColumnTypeName::PgBox,
16777            "polygon" => ColumnTypeName::Polygon,
16778            "line" => ColumnTypeName::Line,
16779            "circle" => ColumnTypeName::Circle,
16780            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16781            "inet" => ColumnTypeName::Inet,
16782            "cidr" => ColumnTypeName::Cidr,
16783            "macaddr" => ColumnTypeName::Macaddr,
16784            "macaddr8" => ColumnTypeName::Macaddr8,
16785            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16786            // width in the value, so the optional `(N)` typmod is accepted and
16787            // ignored (the column stores whatever width it's given).
16788            "bit" => {
16789                let varying = matches!(
16790                    self.peek(),
16791                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16792                );
16793                if varying {
16794                    self.advance();
16795                }
16796                // v7.39 (round 281) — the length used to be parsed and
16797                // dropped, so `bit(3)` accepted a five-bit string.
16798                let n = if matches!(self.peek(), Token::LParen) {
16799                    self.parse_paren_size("BIT")?
16800                } else {
16801                    0
16802                };
16803                if varying {
16804                    ColumnTypeName::BitVarying(n)
16805                } else {
16806                    ColumnTypeName::Bit(n)
16807                }
16808            }
16809            "varbit" => {
16810                let n = if matches!(self.peek(), Token::LParen) {
16811                    self.parse_paren_size("VARBIT")?
16812                } else {
16813                    0
16814                };
16815                ColumnTypeName::BitVarying(n)
16816            }
16817            "xml" => ColumnTypeName::Xml,
16818            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16819            "hstore" => ColumnTypeName::Hstore,
16820            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16821            // `ENUM('a','b','c')`. Storage is TEXT; the value
16822            // list lands on `inline_enum_variants` for the
16823            // engine to validate INSERT cells against. Empty
16824            // value list is a parse error (matches MySQL).
16825            "enum" => {
16826                // Expect the opening `(`.
16827                if !matches!(self.peek(), Token::LParen) {
16828                    return Err(self.err(alloc::format!(
16829                        "expected '(' after ENUM, got {:?}",
16830                        self.peek()
16831                    )));
16832                }
16833                self.advance();
16834                let mut variants: Vec<String> = Vec::new();
16835                loop {
16836                    match self.advance() {
16837                        Token::String(s) => variants.push(s),
16838                        other => {
16839                            return Err(self.err(alloc::format!(
16840                                "ENUM(...) expects string literal variants, got {other:?}"
16841                            )));
16842                        }
16843                    }
16844                    match self.peek() {
16845                        Token::Comma => {
16846                            self.advance();
16847                            continue;
16848                        }
16849                        Token::RParen => {
16850                            self.advance();
16851                            break;
16852                        }
16853                        other => {
16854                            return Err(self.err(alloc::format!(
16855                                "expected ',' or ')' in ENUM(...), got {other:?}"
16856                            )));
16857                        }
16858                    }
16859                }
16860                if variants.is_empty() {
16861                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
16862                }
16863                inline_enum_variants = Some(variants);
16864                // Storage is plain TEXT; the variant list lives on
16865                // the ColumnSchema side.
16866                ColumnTypeName::Text
16867            }
16868            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16869            // `SET('a','b','c')`. Same parse shape as ENUM;
16870            // semantics differ (subset rather than pick-one).
16871            "set" => {
16872                if !matches!(self.peek(), Token::LParen) {
16873                    return Err(self.err(alloc::format!(
16874                        "expected '(' after SET, got {:?}",
16875                        self.peek()
16876                    )));
16877                }
16878                self.advance();
16879                let mut variants: Vec<String> = Vec::new();
16880                loop {
16881                    match self.advance() {
16882                        Token::String(s) => variants.push(s),
16883                        other => {
16884                            return Err(self.err(alloc::format!(
16885                                "SET(...) expects string literal variants, got {other:?}"
16886                            )));
16887                        }
16888                    }
16889                    match self.peek() {
16890                        Token::Comma => {
16891                            self.advance();
16892                            continue;
16893                        }
16894                        Token::RParen => {
16895                            self.advance();
16896                            break;
16897                        }
16898                        other => {
16899                            return Err(self.err(alloc::format!(
16900                                "expected ',' or ')' in SET(...), got {other:?}"
16901                            )));
16902                        }
16903                    }
16904                }
16905                if variants.is_empty() {
16906                    return Err(self.err("SET(...) must declare at least one variant".into()));
16907                }
16908                inline_set_variants = Some(variants);
16909                ColumnTypeName::Text
16910            }
16911            _other => {
16912                // v7.17.0 Phase 1.4 — unknown ident → defer
16913                // resolution to the engine. Stored as Text in
16914                // ColumnTypeName + the original name carried as
16915                // `user_type_ref` so CREATE TABLE can look up
16916                // user-defined enum / domain types.
16917                user_type_ref = Some(ty_ident.clone());
16918                ColumnTypeName::Text
16919            }
16920        };
16921        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16922        // right after the type keyword. Pre-4.4 SPG consumed +
16923        // discarded the keyword, leaving a customer column
16924        // declared `id INT UNSIGNED NOT NULL` silently accepting
16925        // negative values — a Tier-A correctness drift where
16926        // application invariants (auto-increment-IDs never
16927        // negative) silently broke on cutover. Now: capture as
16928        // a column flag, persist on the schema, enforce at
16929        // INSERT / UPDATE time.
16930        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16931        {
16932            self.advance();
16933            true
16934        } else {
16935            false
16936        };
16937        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16938        // `<type> COLLATE <name>` post-fixes on text columns. SPG
16939        // stores text as UTF-8 always so CHARACTER SET is still a
16940        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16941        // name: it gets classified into a `Collation` variant the
16942        // engine consults at WHERE-eval time. PG `default` /
16943        // `pg_catalog.default` / `C` / `POSIX` collations all
16944        // resolve to `Binary` (the prior behaviour); `_ci` /
16945        // `case_insensitive` / `nocase` shift to CaseInsensitive.
16946        // The schema-qualifier form (`pg_catalog.default`) lexes
16947        // as `Ident '.' Ident` — peek for the `.` and consume both
16948        // halves so it's treated as one collation name. PG's
16949        // `IDENT.IDENT` collation form (which can appear here) is
16950        // resolved by Collation::from_collation_name on the bare
16951        // identifier after the dot.
16952        let mut collation = Collation::Binary;
16953        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16954        // clause was written. The engine needs this to tell an explicit
16955        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16956        // clause at all: both resolve to `Collation::Binary`, but under the
16957        // MySQL dialect the latter takes the folding default collation.
16958        let mut collation_explicit = false;
16959        let mut collation_name: Option<alloc::string::String> = None;
16960        loop {
16961            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16962                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16963            {
16964                self.advance(); // CHARACTER
16965                self.advance(); // SET
16966                if matches!(
16967                    self.peek(),
16968                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16969                ) {
16970                    self.advance();
16971                }
16972                continue;
16973            }
16974            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16975                self.advance(); // COLLATE
16976                // Accept Ident / QuotedIdent / String AND the
16977                // keyword-tokenised `Default` (PG `pg_catalog.default`
16978                // and bare `DEFAULT` collation names — `default` is a
16979                // reserved word so the lexer hands back Token::Default
16980                // not Token::Ident).
16981                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16982                    match this.peek().clone() {
16983                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16984                            this.advance();
16985                            Some(s)
16986                        }
16987                        Token::Default => {
16988                            this.advance();
16989                            Some(alloc::string::String::from("default"))
16990                        }
16991                        _ => None,
16992                    }
16993                };
16994                let raw = if let Some(head) = read_collation_atom(self) {
16995                    // Schema-qualified PG form: `pg_catalog.default`.
16996                    if matches!(self.peek(), Token::Dot) {
16997                        self.advance();
16998                        let tail = read_collation_atom(self).unwrap_or_default();
16999                        alloc::format!("{head}.{tail}")
17000                    } else {
17001                        head
17002                    }
17003                } else {
17004                    alloc::string::String::new()
17005                };
17006                if !raw.is_empty() {
17007                    collation_explicit = true;
17008                    // v7.39 (round 676) — keep the name too. The enum below
17009                    // folds C / POSIX / en_US / default into one value, and
17010                    // `pg_attribute.attcollation` has to tell them apart.
17011                    // The schema qualifier goes: PG's `pg_catalog.default`
17012                    // and a bare `default` name the same collation.
17013                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17014                    // encoding suffix. Round 676 used `rsplit('.')` for
17015                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17016                    // PG writes `pg_catalog.default` (qualifier) and
17017                    // `en_US.utf8` (locale + encoding) with the same
17018                    // separator. Only `pg_catalog.` is a qualifier, and it
17019                    // is the only one PG's own dumps emit.
17020                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17021                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17022                    collation_name = Some(alloc::string::String::from(bare));
17023                    let parsed = Collation::from_collation_name(&raw);
17024                    // Last COLLATE clause wins, but `Binary` from a
17025                    // bare keyword like `default` should not
17026                    // silently downgrade a stronger one set earlier
17027                    // on the same column. v7.17 only ships one
17028                    // non-Binary variant so a simple OR is enough.
17029                    if parsed != Collation::Binary {
17030                        collation = parsed;
17031                    }
17032                }
17033                continue;
17034            }
17035            break;
17036        }
17037        // v7.10.10 — postfix `[]` widens the base type to its array
17038        // type. PG accepts `TYPE[]` after any base type and so does
17039        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17040        // all through; the old "only TEXT[]" note was stale).
17041        if matches!(self.peek(), Token::LBracket) {
17042            self.advance();
17043            if !matches!(self.peek(), Token::RBracket) {
17044                return Err(self.err(alloc::format!(
17045                    "TEXT[] takes no dimension; got {:?}",
17046                    self.peek()
17047                )));
17048            }
17049            self.advance();
17050            // v7.11.13 — widened to INT[] and BIGINT[] in addition
17051            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17052            // still error here.
17053            ty = match ty {
17054                ColumnTypeName::Text => ColumnTypeName::TextArray,
17055                ColumnTypeName::Int => ColumnTypeName::IntArray,
17056                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17057                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17058                // `[]` grammar. Wire OID 1187.
17059                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17060                // v7.37.5 γ — full PG array-of-scalar family.
17061                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17062                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17063                ColumnTypeName::Float => ColumnTypeName::FloatArray,
17064                // NUMERIC(p, s) loses its precision params at the
17065                // array level (matches PG: `NUMERIC[]` is untyped,
17066                // per-element precision flows through values).
17067                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17068                ColumnTypeName::Date => ColumnTypeName::DateArray,
17069                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17070                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17071                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17072                ColumnTypeName::Json => ColumnTypeName::JsonArray,
17073                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17074                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17075                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17076                // the array level (matches PG semantics where the
17077                // element precision is per-row, not column-wide).
17078                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17079                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17080                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17081                // follow-up.
17082                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17083                other => {
17084                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17085                }
17086            };
17087            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17088            // for INT/TEXT/BIGINT. Anything else is an error.
17089            if matches!(self.peek(), Token::LBracket) {
17090                self.advance();
17091                if !matches!(self.peek(), Token::RBracket) {
17092                    return Err(self.err(alloc::format!(
17093                        "TYPE[][] second dimension takes no size; got {:?}",
17094                        self.peek()
17095                    )));
17096                }
17097                self.advance();
17098                ty = match ty {
17099                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17100                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17101                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17102                    // v7.39 (read01 round 75) — bool[][].
17103                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17104                    other => {
17105                        return Err(self.err(alloc::format!(
17106                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17107                             TEXT[][] only; got {other:?}"
17108                        )));
17109                    }
17110                };
17111            }
17112        }
17113        Ok((
17114            ty,
17115            implied_auto_increment,
17116            implied_not_null,
17117            user_type_ref,
17118            collation,
17119            collation_explicit,
17120            collation_name,
17121            is_unsigned,
17122            inline_enum_variants,
17123            inline_set_variants,
17124            mysql_int_width,
17125            mysql_fsp,
17126        ))
17127    }
17128
17129    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17130        // v7.20 — PG reserves the table-constraint keywords, so a
17131        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17132        // malformed constraint clause (e.g. `UNIQUE a` missing its
17133        // parens), not a column named "unique". Since v7.17's
17134        // unknown-type leniency (`user_type_ref`) such a clause
17135        // would otherwise parse as a column with a user-defined
17136        // type — silently accepting invalid DDL. Quoted
17137        // identifiers ("unique" / `unique`) remain valid names.
17138        if let Token::Ident(s) = self.peek()
17139            && [
17140                "unique",
17141                "primary",
17142                "foreign",
17143                "constraint",
17144                "check",
17145                "references",
17146                "exclude",
17147            ]
17148            .iter()
17149            .any(|kw| s.eq_ignore_ascii_case(kw))
17150        {
17151            return Err(self.err(alloc::format!(
17152                "unexpected reserved keyword '{s}' at start of column definition \
17153                 (malformed table constraint?)"
17154            )));
17155        }
17156        let name = self.expect_ident_like()?;
17157        let (
17158            ty,
17159            implied_auto_increment,
17160            implied_not_null,
17161            user_type_ref,
17162            collation,
17163            collation_explicit,
17164            collation_name,
17165            is_unsigned,
17166            inline_enum_variants,
17167            inline_set_variants,
17168            mysql_int_width,
17169            mysql_fsp,
17170        ) = self.parse_type_with_implied_flags()?;
17171        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17172        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17173        // each at most once.
17174        let mut default: Option<Expr> = None;
17175        let mut nullable = !implied_not_null;
17176        let mut nullability_seen = implied_not_null;
17177        let mut auto_increment = implied_auto_increment;
17178        let mut is_primary_key = false;
17179        let mut is_unique = false;
17180        let mut unique_nulls_not_distinct = false;
17181        let mut constraint_deferrable = false;
17182        let mut constraint_initially_deferred = false;
17183        let mut check: Option<Expr> = None;
17184        let mut on_update_runtime: Option<Expr> = None;
17185        let mut generated_stored_expr: Option<Box<Expr>> = None;
17186        let mut identity_always = false;
17187        loop {
17188            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17189            // not-null constraints by name and pg_dump emits them
17190            // inline: `id bigint CONSTRAINT contacts_id_not_null1
17191            // NOT NULL`. Accept and discard the name; whatever
17192            // constraint follows is parsed by the arms below.
17193            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17194                // v7.39 (round 308, V29) — a name on an inline
17195                // REFERENCES belongs to the FOREIGN KEY, and the caller
17196                // (`parse_column_def_with_fk`) is what builds it, so
17197                // leave the whole clause for it. Dropping the name here
17198                // is what made `CONSTRAINT fk_a REFERENCES …` come back
17199                // as the synthesised `c_pid_fkey` — which then could
17200                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17201                // `advance()` takes tokens by `mem::replace`, so there
17202                // is no rewinding once consumed.
17203                if matches!(
17204                    self.tokens.get(self.pos + 2),
17205                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17206                ) {
17207                    break;
17208                }
17209                self.advance();
17210                let _name = self.expect_ident_like()?;
17211                continue;
17212            }
17213            // v7.39 (round 379) — MySQL's SHORT generated-column form
17214            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17215            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17216            // below), but hand-written schemas and app migrations use this.
17217            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17218            // SPG computes-and-stores either way, like the long form.
17219            if matches!(self.peek(), Token::As) {
17220                self.advance();
17221                if !matches!(self.peek(), Token::LParen) {
17222                    return Err(self.err(alloc::format!(
17223                        "expected '(' after AS in a generated column, got {:?}",
17224                        self.peek()
17225                    )));
17226                }
17227                self.advance();
17228                let expr = self.parse_expr(0)?;
17229                if !matches!(self.peek(), Token::RParen) {
17230                    return Err(self.err(alloc::format!(
17231                        "expected ')' after AS (<expr>), got {:?}",
17232                        self.peek()
17233                    )));
17234                }
17235                self.advance();
17236                if matches!(self.peek(), Token::Ident(s)
17237                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17238                {
17239                    self.advance();
17240                }
17241                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17242                continue;
17243            }
17244            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17245            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17246            // the modern replacement for SERIAL in hand-written
17247            // schemas). Both flavours map onto the auto-increment
17248            // machinery — SPG's serial semantics ≈ BY DEFAULT;
17249            // ALWAYS's reject-explicit-values nuance is documented
17250            // leniency. Generated EXPRESSION columns
17251            // (`AS (expr) STORED`) are not supported: error loudly
17252            // instead of silently storing NULLs.
17253            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17254                self.advance();
17255                let mut saw_generated_always = false;
17256                match self.peek().clone() {
17257                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17258                        self.advance();
17259                        saw_generated_always = true;
17260                    }
17261                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17262                        self.advance();
17263                        if !matches!(self.peek(), Token::Default) {
17264                            return Err(self.err(alloc::format!(
17265                                "expected DEFAULT after GENERATED BY, got {:?}",
17266                                self.peek()
17267                            )));
17268                        }
17269                        self.advance();
17270                    }
17271                    other => {
17272                        return Err(self.err(alloc::format!(
17273                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17274                        )));
17275                    }
17276                }
17277                if !matches!(self.peek(), Token::As) {
17278                    return Err(self.err(alloc::format!(
17279                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17280                        self.peek()
17281                    )));
17282                }
17283                self.advance();
17284                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17285                // ( <expr> ) STORED` stored computed-column. The
17286                // expression is captured for the engine to recompute
17287                // on every INSERT / UPDATE. v7.37.7 accepts the
17288                // STORED keyword only; PG also has VIRTUAL, which
17289                // v7.37.7 carves out (sentori only uses STORED).
17290                if matches!(self.peek(), Token::LParen) {
17291                    self.advance();
17292                    let expr = self.parse_expr(0)?;
17293                    if !matches!(self.peek(), Token::RParen) {
17294                        return Err(self.err(alloc::format!(
17295                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17296                            self.peek()
17297                        )));
17298                    }
17299                    self.advance();
17300                    let stored = match self.peek() {
17301                        Token::Ident(s) | Token::QuotedIdent(s)
17302                            if s.eq_ignore_ascii_case("stored") =>
17303                        {
17304                            self.advance();
17305                            true
17306                        }
17307                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17308                        // generated columns. SPG computes them on write and
17309                        // persists like STORED; the two are observably
17310                        // identical for query results (the value, recompute
17311                        // on base-column change, and NOT NULL enforcement all
17312                        // match), so a PG 18 schema/dump using VIRTUAL loads
17313                        // and behaves correctly. The compute-on-read storage
17314                        // saving is an invisible internal difference.
17315                        Token::Ident(s) | Token::QuotedIdent(s)
17316                            if s.eq_ignore_ascii_case("virtual") =>
17317                        {
17318                            self.advance();
17319                            false
17320                        }
17321                        other => {
17322                            return Err(self.err(alloc::format!(
17323                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17324                                 got {other:?}"
17325                            )));
17326                        }
17327                    };
17328                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
17329                    generated_stored_expr = Some(Box::new(expr));
17330                    continue;
17331                }
17332                self.expect_keyword_ident("identity")?;
17333                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17334                // consume the balanced parens and discard (SPG's
17335                // auto-increment is max+1-scan based).
17336                if matches!(self.peek(), Token::LParen) {
17337                    let mut depth = 0usize;
17338                    loop {
17339                        match self.advance() {
17340                            Token::LParen => depth += 1,
17341                            Token::RParen => {
17342                                depth -= 1;
17343                                if depth == 0 {
17344                                    break;
17345                                }
17346                            }
17347                            Token::Eof => {
17348                                return Err(self.err(
17349                                    "unterminated sequence-options parens after IDENTITY".into(),
17350                                ));
17351                            }
17352                            _ => {}
17353                        }
17354                    }
17355                }
17356                auto_increment = true;
17357                // v7.38 (read01) — remember the ALWAYS flavour so the engine
17358                // can reject explicit non-DEFAULT INSERT values (unless
17359                // OVERRIDING SYSTEM VALUE) the way PG does.
17360                identity_always = saw_generated_always;
17361                // PG identity columns are implicitly NOT NULL.
17362                nullable = false;
17363                continue;
17364            }
17365            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17366            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17367            // is accepted today. The "ON" token is an Ident
17368            // (not reserved) — peek before consuming.
17369            if matches!(self.peek(), Token::On)
17370                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17371            {
17372                self.advance(); // ON
17373                self.advance(); // update
17374                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17375                let next = self.peek().clone();
17376                match next {
17377                    Token::Ident(s) | Token::QuotedIdent(s)
17378                        if s.eq_ignore_ascii_case("current_timestamp") =>
17379                    {
17380                        self.advance();
17381                        // Optional `(N)` precision.
17382                        if matches!(self.peek(), Token::LParen) {
17383                            self.advance();
17384                            if !matches!(self.peek(), Token::Integer(_)) {
17385                                return Err(self.err(alloc::format!(
17386                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17387                                    self.peek()
17388                                )));
17389                            }
17390                            self.advance();
17391                            if !matches!(self.peek(), Token::RParen) {
17392                                return Err(self.err(alloc::format!(
17393                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17394                                    self.peek()
17395                                )));
17396                            }
17397                            self.advance();
17398                        }
17399                        on_update_runtime = Some(Expr::FunctionCall {
17400                            name: "now".into(),
17401                            args: Vec::new(),
17402                        });
17403                        continue;
17404                    }
17405                    other => {
17406                        return Err(self.err(alloc::format!(
17407                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17408                        )));
17409                    }
17410                }
17411            }
17412            if matches!(self.peek(), Token::Default) {
17413                if default.is_some() {
17414                    return Err(self.err("DEFAULT specified twice".into()));
17415                }
17416                self.advance();
17417                default = Some(self.parse_expr(0)?);
17418                continue;
17419            }
17420            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17421            // token with NOT NULL and sits EARLIER in the loop than the
17422            // deferrability arm, so without the lookahead it was reported as
17423            // "NOT NULL specified twice" (or "expected NULL after NOT").
17424            if matches!(self.peek(), Token::Not)
17425                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17426            {
17427                // NOT DEFERRABLE — explicit immediate; nothing to carry.
17428                self.consume_optional_deferrable_clauses()?;
17429                continue;
17430            }
17431            if matches!(self.peek(), Token::Not) {
17432                if nullability_seen {
17433                    return Err(self.err("NOT NULL specified twice".into()));
17434                }
17435                self.advance();
17436                if !matches!(self.peek(), Token::Null) {
17437                    return Err(self.err(format!(
17438                        "expected NULL after NOT in column def, got {:?}",
17439                        self.peek()
17440                    )));
17441                }
17442                self.advance();
17443                nullable = false;
17444                nullability_seen = true;
17445                continue;
17446            }
17447            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17448            // "this column is nullable" marker (the default in
17449            // standard SQL anyway). mysqldump emits it routinely
17450            // (`col TYPE NULL DEFAULT NULL` for nullable
17451            // timestamps etc). Accept + no-op.
17452            if matches!(self.peek(), Token::Null) {
17453                if nullability_seen && !nullable {
17454                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
17455                    // sentence, PG18-measured (the table name is the
17456                    // caller's; the column half is exact).
17457                    return Err(self.err(alloc::format!(
17458                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
17459                    )));
17460                }
17461                self.advance();
17462                nullable = true;
17463                nullability_seen = true;
17464                continue;
17465            }
17466            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17467            // arrives as a bare Ident. Match either, case-insensitive.
17468            if let Token::Ident(s) = self.peek()
17469                && (s.eq_ignore_ascii_case("auto_increment")
17470                    || s.eq_ignore_ascii_case("autoincrement"))
17471            {
17472                if auto_increment {
17473                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
17474                }
17475                self.advance();
17476                auto_increment = true;
17477                continue;
17478            }
17479            // v7.9.13 — inline `PRIMARY KEY` column constraint
17480            // (mailrs F1). Implies `NOT NULL`. The engine creates
17481            // a BTree index for the PK column at CREATE TABLE time
17482            // so FK parent-side index lookups resolve.
17483            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17484            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17485            // spelling was a parse error, so a pg_dump carrying one stopped
17486            // mid-restore. The clauses are consumed by the same helper the FK
17487            // path has used since round 288 and recorded nowhere: SPG enforces
17488            // the constraint IMMEDIATELY either way, which fails earlier than
17489            // PG inside a transaction that violates-then-repairs — a refusal,
17490            // not a wrong answer. True deferral is the open remainder of F08.
17491            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17492                || (matches!(self.peek(), Token::Not)
17493                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17494            {
17495                // v7.39 (round 711) — CARRIED now (the storing half of
17496                // F08); round 621 only consumed.
17497                let (d, idef) = self.consume_deferrable_clauses_timed()?;
17498                constraint_deferrable |= d;
17499                constraint_initially_deferred |= idef;
17500                continue;
17501            }
17502            if let Token::Ident(s) = self.peek()
17503                && s.eq_ignore_ascii_case("primary")
17504            {
17505                if is_primary_key {
17506                    return Err(self.err("PRIMARY KEY specified twice".into()));
17507                }
17508                // Peek-ahead for the required `KEY` token.
17509                let next = self.tokens.get(self.pos + 1);
17510                let next_is_key = matches!(
17511                    next,
17512                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17513                );
17514                if !next_is_key {
17515                    return Err(self.err(format!(
17516                        "expected KEY after PRIMARY in column def, got {:?}",
17517                        next
17518                    )));
17519                }
17520                self.advance(); // PRIMARY
17521                self.advance(); // KEY
17522                is_primary_key = true;
17523                if nullability_seen && nullable {
17524                    return Err(self.err(
17525                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17526                    ));
17527                }
17528                nullable = false;
17529                nullability_seen = true;
17530                continue;
17531            }
17532            // v7.13.0 — inline `UNIQUE` column constraint
17533            // (mailrs round-5 G2). Fold into a single-column
17534            // table-level UNIQUE at CREATE TABLE post-process time.
17535            if let Token::Ident(s) = self.peek()
17536                && s.eq_ignore_ascii_case("unique")
17537            {
17538                if is_unique {
17539                    return Err(self.err("UNIQUE specified twice".into()));
17540                }
17541                self.advance();
17542                is_unique = true;
17543                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17544                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17545                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17546                    let n1 = self.tokens.get(self.pos + 1);
17547                    let n2 = self.tokens.get(self.pos + 2);
17548                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17549                        self.advance(); // NULLS
17550                        self.advance(); // NOT
17551                        self.advance(); // DISTINCT
17552                        unique_nulls_not_distinct = true;
17553                    } else if matches!(n1, Some(Token::Distinct)) {
17554                        self.advance(); // NULLS
17555                        self.advance(); // DISTINCT
17556                    }
17557                }
17558                continue;
17559            }
17560            // v7.13.0 — inline `CHECK (<expr>)` column constraint
17561            // (mailrs round-5 G3). PG semantics: column-level
17562            // CHECK is equivalent to a table-level CHECK. Multiple
17563            // inline CHECKs on the same column AND together.
17564            if let Token::Ident(s) = self.peek()
17565                && s.eq_ignore_ascii_case("check")
17566            {
17567                self.advance();
17568                if !matches!(self.peek(), Token::LParen) {
17569                    return Err(self.err(alloc::format!(
17570                        "expected '(' after CHECK in column def, got {:?}",
17571                        self.peek()
17572                    )));
17573                }
17574                self.advance();
17575                let pred = self.parse_expr(0)?;
17576                if !matches!(self.peek(), Token::RParen) {
17577                    return Err(self.err(alloc::format!(
17578                        "expected ')' to close CHECK predicate, got {:?}",
17579                        self.peek()
17580                    )));
17581                }
17582                self.advance();
17583                check = Some(match check.take() {
17584                    Some(prev) => Expr::Binary {
17585                        op: BinOp::And,
17586                        lhs: Box::new(prev),
17587                        rhs: Box::new(pred),
17588                    },
17589                    None => pred,
17590                });
17591                continue;
17592            }
17593            break;
17594        }
17595        Ok(ColumnDef {
17596            name,
17597            ty,
17598            nullable,
17599            default,
17600            auto_increment,
17601            is_primary_key,
17602            is_unique,
17603            unique_nulls_not_distinct,
17604            constraint_deferrable,
17605            constraint_initially_deferred,
17606            check,
17607            user_type_ref,
17608            on_update_runtime,
17609            collation,
17610            collation_explicit,
17611            collation_name,
17612            is_unsigned,
17613            inline_enum_variants,
17614            inline_set_variants,
17615            generated_stored_expr,
17616            identity_always,
17617            mysql_int_width,
17618            mysql_fsp,
17619        })
17620    }
17621
17622    /// `NUMERIC` may appear without parameters, with one (precision
17623    /// only, scale=0), or with both. Returns `(precision, scale)` with
17624    /// 0 = unspecified for the bare form.
17625    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17626        if !matches!(self.peek(), Token::LParen) {
17627            // Bare `NUMERIC` — PG treats this as "unlimited precision";
17628            // we surface it as precision=0 to mean "unconstrained" so
17629            // the engine doesn't need a separate variant.
17630            return Ok((0, 0));
17631        }
17632        self.advance();
17633        // v7.39 (round 272) — PG's declared precision runs to 1000, and
17634        // it words the out-of-range case with the value it saw. SPG
17635        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17636        // accepts failed to parse at all; values wider than i128 are
17637        // carried by the arbitrary-precision form.
17638        let precision = match self.advance() {
17639            Token::Integer(n) if (1..=1000).contains(&n) => {
17640                u16::try_from(n).expect("range-checked")
17641            }
17642            Token::Integer(n) => {
17643                return Err(ParseError {
17644                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17645                    token_pos: self.consumed_pos(),
17646                });
17647            }
17648            other => {
17649                return Err(ParseError {
17650                    message: format!(
17651                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17652                    ),
17653                    token_pos: self.consumed_pos(),
17654                });
17655            }
17656        };
17657        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17658        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17659        // then overflows). A negative scale rounds to tens / hundreds / …
17660        let scale = if matches!(self.peek(), Token::Comma) {
17661            self.advance();
17662            let neg = if matches!(self.peek(), Token::Minus) {
17663                self.advance();
17664                true
17665            } else {
17666                false
17667            };
17668            match self.advance() {
17669                Token::Integer(n) => {
17670                    let signed = if neg { -n } else { n };
17671                    if !(-1000..=1000).contains(&signed) {
17672                        return Err(ParseError {
17673                            message: format!(
17674                                "NUMERIC scale {signed} must be between -1000 and 1000"
17675                            ),
17676                            token_pos: self.consumed_pos(),
17677                        });
17678                    }
17679                    i16::try_from(signed).expect("range-checked")
17680                }
17681                other => {
17682                    return Err(ParseError {
17683                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
17684                        token_pos: self.consumed_pos(),
17685                    });
17686                }
17687            }
17688        } else {
17689            0
17690        };
17691        if !matches!(self.peek(), Token::RParen) {
17692            return Err(self.err(format!(
17693                "expected ')' to close NUMERIC params, got {:?}",
17694                self.peek()
17695            )));
17696        }
17697        self.advance();
17698        Ok((precision, scale))
17699    }
17700
17701    /// Parse `(N)` where `N` is a positive integer literal — used by the
17702    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17703    /// for the error message.
17704    /// v6.0.1: parse the optional `USING <encoding>` clause that
17705    /// follows `VECTOR(N)` in a column definition. Missing clause
17706    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17707    /// ident → `ParseError` listing the encodings recognised today.
17708    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17709        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17710            return Ok(VecEncoding::F32);
17711        }
17712        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17713        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17714        // consume the token when the very next token is a known
17715        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17716        // `USING` for the caller — it's the rewrite-expression form.
17717        let n1 = self.tokens.get(self.pos + 1);
17718        let next_is_encoding = matches!(
17719            n1,
17720            Some(Token::Ident(s))
17721                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17722        );
17723        if !next_is_encoding {
17724            return Ok(VecEncoding::F32);
17725        }
17726        self.advance();
17727        let enc_ident = match self.advance() {
17728            Token::Ident(s) => s,
17729            other => {
17730                return Err(self.err(format!(
17731                    "expected vector encoding after USING, got {other:?}"
17732                )));
17733            }
17734        };
17735        match enc_ident.to_ascii_lowercase().as_str() {
17736            "sq8" => Ok(VecEncoding::Sq8),
17737            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17738            // binary16 per-element storage.
17739            "half" => Ok(VecEncoding::F16),
17740            other => Err(self.err(format!(
17741                "unknown vector encoding {other:?}; supported: SQ8, HALF"
17742            ))),
17743        }
17744    }
17745
17746    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17747    /// without consuming it. Returns `Some(N)` when the next
17748    /// tokens are `( <int> )`; None otherwise. Used by the
17749    /// TINYINT classifier to decide whether to map to Bool or
17750    /// SmallInt.
17751    fn peek_optional_paren_size_value(&self) -> Option<i64> {
17752        if !matches!(self.peek(), Token::LParen) {
17753            return None;
17754        }
17755        let next = self.tokens.get(self.pos + 1)?;
17756        let n = match next {
17757            Token::Integer(n) => *n,
17758            _ => return None,
17759        };
17760        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17761            return None;
17762        }
17763        Some(n)
17764    }
17765
17766    /// v7.14.0 — consume an optional MySQL display-width
17767    /// parenthesised number after an integer type, returning
17768    /// nothing. `TINYINT(1)` etc.
17769    /// v7.39 (round 360) — does the parenthesised group ahead contain a
17770    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17771    fn peek_paren_has_comma(&self) -> bool {
17772        let mut i = self.pos + 1;
17773        let mut depth = 1usize;
17774        while depth > 0 {
17775            match self.tokens.get(i) {
17776                Some(Token::LParen) => depth += 1,
17777                Some(Token::RParen) => depth -= 1,
17778                Some(Token::Comma) if depth == 1 => return true,
17779                None | Some(Token::Eof) => return false,
17780                _ => {}
17781            }
17782            i += 1;
17783        }
17784        false
17785    }
17786
17787    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17788    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17789    /// fractional-seconds precision that drives write truncation and render
17790    /// padding, where `consume_optional_paren_size` throws it away.
17791    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17792    fn take_optional_paren_size(&mut self) -> Option<u8> {
17793        let Some(Token::Integer(n)) = self
17794            .tokens
17795            .get(self.pos + 1)
17796            .filter(|_| matches!(self.peek(), Token::LParen))
17797            .cloned()
17798        else {
17799            self.consume_optional_paren_size();
17800            return None;
17801        };
17802        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17803            self.consume_optional_paren_size();
17804            return None;
17805        }
17806        self.consume_optional_paren_size();
17807        u8::try_from(n).ok()
17808    }
17809
17810    fn consume_optional_paren_size(&mut self) {
17811        if !matches!(self.peek(), Token::LParen) {
17812            return;
17813        }
17814        self.advance();
17815        // Skip until matching RParen (allow nested or any tokens).
17816        let mut depth = 1usize;
17817        while depth > 0 {
17818            match self.peek() {
17819                Token::LParen => depth += 1,
17820                Token::RParen => depth -= 1,
17821                Token::Eof => return,
17822                _ => {}
17823            }
17824            self.advance();
17825        }
17826    }
17827
17828    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17829        if !matches!(self.peek(), Token::LParen) {
17830            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17831        }
17832        self.advance();
17833        let n = match self.advance() {
17834            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17835                message: format!("{label} size too large: {n}"),
17836                token_pos: self.consumed_pos(),
17837            })?,
17838            other => {
17839                return Err(ParseError {
17840                    message: format!("expected positive integer {label} size, got {other:?}"),
17841                    token_pos: self.consumed_pos(),
17842                });
17843            }
17844        };
17845        if !matches!(self.peek(), Token::RParen) {
17846            return Err(self.err(format!(
17847                "expected ')' after {label} size, got {:?}",
17848                self.peek()
17849            )));
17850        }
17851        self.advance();
17852        Ok(n)
17853    }
17854
17855    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17856    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17857    /// key, like MySQL) whose action skips conflicting rows.
17858    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17859    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17860    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17861    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17862    /// common bulk-upsert spellings —
17863    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17864    ///     REPLACE INTO t SELECT …
17865    /// — were a parse error / a duplicate-key failure respectively.
17866    ///
17867    /// Precedence: an explicitly written clause beats a statement-level flag.
17868    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17869    /// implicit `REPLACE` and `IGNORE` lowerings.
17870    fn parse_insert_conflict_clause(
17871        &mut self,
17872        replace: bool,
17873        ignore: bool,
17874    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17875        if let Some(c) = self.parse_optional_on_duplicate_key()? {
17876            return Ok(Some(c));
17877        }
17878        if let Some(c) = self.parse_optional_on_conflict()? {
17879            return Ok(Some(c));
17880        }
17881        if replace {
17882            // REPLACE INTO = delete-then-insert, which PG spells as
17883            // `ON CONFLICT DO UPDATE SET` over every column; the engine
17884            // reads an empty assignment list as "take the incoming row".
17885            return Ok(Some(crate::ast::OnConflictClause {
17886                target_columns: Vec::new(),
17887                index_where: None,
17888                constraint_name: None,
17889                mysql_lowered: true,
17890                action: crate::ast::OnConflictAction::Update {
17891                    assignments: Vec::new(),
17892                    where_: None,
17893                },
17894            }));
17895        }
17896        if ignore {
17897            return Ok(Some(Self::insert_ignore_clause()));
17898        }
17899        Ok(None)
17900    }
17901
17902    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17903    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17904    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17905    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17906    fn parse_optional_on_duplicate_key(
17907        &mut self,
17908    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17909        if !(matches!(self.peek(), Token::On)
17910            && matches!(self.tokens.get(self.pos + 1),
17911                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17912        {
17913            return Ok(None);
17914        }
17915        self.advance(); // ON
17916        self.advance(); // DUPLICATE
17917        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17918            return Err(self.err(format!(
17919                "expected KEY after ON DUPLICATE, got {:?}",
17920                self.peek()
17921            )));
17922        }
17923        self.advance();
17924        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17925            return Err(self.err(format!(
17926                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17927                self.peek()
17928            )));
17929        }
17930        self.advance();
17931        let mut assignments: Vec<(String, Expr)> = Vec::new();
17932        loop {
17933            let col = self.expect_ident_like()?;
17934            if !matches!(self.peek(), Token::Eq) {
17935                return Err(self.err(format!(
17936                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17937                    self.peek()
17938                )));
17939            }
17940            self.advance();
17941            let mut expr = self.parse_expr(0)?;
17942            Self::rewrite_mysql_values_refs(&mut expr);
17943            assignments.push((col, expr));
17944            if matches!(self.peek(), Token::Comma) {
17945                self.advance();
17946                continue;
17947            }
17948            break;
17949        }
17950        Ok(Some(crate::ast::OnConflictClause {
17951            target_columns: Vec::new(),
17952            index_where: None,
17953            constraint_name: None,
17954            mysql_lowered: true,
17955            action: crate::ast::OnConflictAction::Update {
17956                assignments,
17957                where_: None,
17958            },
17959        }))
17960    }
17961
17962    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17963        crate::ast::OnConflictClause {
17964            target_columns: Vec::new(),
17965            index_where: None,
17966            constraint_name: None,
17967            mysql_lowered: true,
17968            action: crate::ast::OnConflictAction::Nothing,
17969        }
17970    }
17971
17972    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17973        debug_assert!(
17974            matches!(self.peek(), Token::Insert)
17975                || (replace
17976                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17977        );
17978        self.advance();
17979        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17980        // would raise a duplicate-key error instead of failing the statement,
17981        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17982        // plain ident to the lexer; only the MySQL dialect accepts it here.
17983        let ignore = self.mysql_dialect
17984            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17985        if ignore {
17986            self.advance();
17987        }
17988        if !matches!(self.peek(), Token::Into) {
17989            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17990        }
17991        self.advance();
17992        let table = self.expect_ident_like()?;
17993        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17994        // grammar requires the AS keyword here (a bare identifier would be
17995        // ambiguous with a column list). The alias is what the ON CONFLICT
17996        // DO UPDATE expressions refer to the target row by.
17997        let alias = if matches!(self.peek(), Token::As) {
17998            self.advance();
17999            Some(self.expect_ident_like()?)
18000        } else {
18001            None
18002        };
18003        // v7.39 (round 428) — MySQL's SET-form INSERT:
18004        //     INSERT INTO t SET a = 1, b = 'x'
18005        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18006        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18007        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18008        // measured). So it lowers to the column list + one VALUES row and
18009        // rejoins the ordinary path, which already handles every one of
18010        // those. PG has no such spelling, hence the dialect gate.
18011        if self.mysql_dialect
18012            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18013        {
18014            self.advance(); // SET
18015            let mut names = Vec::new();
18016            let mut values = Vec::new();
18017            loop {
18018                names.push(self.expect_ident_like()?);
18019                if !matches!(self.peek(), Token::Eq) {
18020                    return Err(self.err(alloc::format!(
18021                        "expected '=' in INSERT … SET, got {:?}",
18022                        self.peek()
18023                    )));
18024                }
18025                self.advance();
18026                // `SET a = DEFAULT` rides the same `__column_default` marker
18027                // the VALUES-row and UPDATE-SET paths use; the INSERT
18028                // executor resolves it against the target column.
18029                if matches!(self.peek(), Token::Default) {
18030                    self.advance();
18031                    values.push(Expr::FunctionCall {
18032                        name: "__column_default".to_string(),
18033                        args: Vec::new(),
18034                    });
18035                } else {
18036                    values.push(self.parse_expr(0)?);
18037                }
18038                if matches!(self.peek(), Token::Comma) {
18039                    self.advance();
18040                    continue;
18041                }
18042                break;
18043            }
18044            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18045            let returning = self.parse_optional_returning()?;
18046            return Ok(Statement::Insert(InsertStatement {
18047                ctes: Vec::new(),
18048                table,
18049                alias,
18050                columns: Some(names),
18051                rows: alloc::vec![values],
18052                select_source: None,
18053                // MySQL's SET form has no `OVERRIDING …` clause (that is
18054                // PG's identity-column spelling).
18055                overriding: Overriding::None,
18056                mysql_ignore: ignore,
18057                on_conflict,
18058                returning,
18059            }));
18060        }
18061        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18062        // v7.39 (round 151) — a SELECT or WITH right after the paren is
18063        // a parenthesized query source instead (PG select_with_parens:
18064        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18065        // both keywords are reserved in PG, so no column list can start
18066        // with them.
18067        let columns = if matches!(self.peek(), Token::LParen) {
18068            self.advance();
18069            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18070                let select_stmt = if self.peek_is_with_kw() {
18071                    self.advance();
18072                    self.parse_nested_with_select()?
18073                } else {
18074                    match self.parse_select_stmt()? {
18075                        Statement::Select(s) => s,
18076                        other => {
18077                            return Err(self.err(alloc::format!(
18078                                "expected SELECT in parenthesized INSERT source, got {other:?}"
18079                            )));
18080                        }
18081                    }
18082                };
18083                if !matches!(self.peek(), Token::RParen) {
18084                    return Err(self.err(format!(
18085                        "expected ')' after parenthesized INSERT source, got {:?}",
18086                        self.peek()
18087                    )));
18088                }
18089                self.advance();
18090                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18091                let returning = self.parse_optional_returning()?;
18092                return Ok(Statement::Insert(InsertStatement {
18093                    ctes: Vec::new(),
18094                    table,
18095                    alias: alias.clone(),
18096                    columns: None,
18097                    rows: Vec::new(),
18098                    select_source: Some(Box::new(select_stmt)),
18099                    on_conflict,
18100                    returning,
18101                    overriding: Overriding::None,
18102                    mysql_ignore: ignore,
18103                }));
18104            }
18105            let mut names = Vec::new();
18106            loop {
18107                names.push(self.expect_ident_like()?);
18108                match self.peek() {
18109                    Token::Comma => {
18110                        self.advance();
18111                    }
18112                    Token::RParen => {
18113                        self.advance();
18114                        break;
18115                    }
18116                    other => {
18117                        return Err(self.err(format!(
18118                            "expected ',' or ')' in INSERT column list, got {other:?}"
18119                        )));
18120                    }
18121                }
18122            }
18123            Some(names)
18124        } else {
18125            None
18126        };
18127        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18128        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18129        // is captured on the statement so the engine can apply PG's
18130        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18131        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18132        {
18133            self.advance();
18134            let which = self.expect_ident_like()?;
18135            let ov = if which.eq_ignore_ascii_case("system") {
18136                Overriding::System
18137            } else if which.eq_ignore_ascii_case("user") {
18138                Overriding::User
18139            } else {
18140                return Err(self.err(format!(
18141                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18142                )));
18143            };
18144            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18145                return Err(self.err(format!(
18146                    "expected VALUE after OVERRIDING {}, got {:?}",
18147                    which.to_ascii_uppercase(),
18148                    self.peek()
18149                )));
18150            }
18151            self.advance();
18152            ov
18153        } else {
18154            Overriding::None
18155        };
18156        // `INSERT INTO t DEFAULT VALUES` — a single row made
18157        // entirely of column defaults. Lower to the permuted
18158        // column-list path with an empty list: every schema column
18159        // is unmapped, so the engine fills each from its default
18160        // (serials advance, plain defaults evaluate, the rest NULL).
18161        if matches!(self.peek(), Token::Default) {
18162            self.advance();
18163            if !matches!(self.peek(), Token::Values) {
18164                return Err(self.err(format!(
18165                    "expected VALUES after DEFAULT in INSERT, got {:?}",
18166                    self.peek()
18167                )));
18168            }
18169            self.advance();
18170            if columns.is_some() {
18171                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18172            }
18173            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18174            let returning = self.parse_optional_returning()?;
18175            return Ok(Statement::Insert(InsertStatement {
18176                ctes: Vec::new(),
18177                table,
18178                alias: alias.clone(),
18179                columns: Some(Vec::new()),
18180                rows: alloc::vec![Vec::new()],
18181                select_source: None,
18182                on_conflict,
18183                returning,
18184                overriding,
18185                mysql_ignore: ignore,
18186            }));
18187        }
18188        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18189        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18190        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18191        // SELECT …`) heads the SOURCE select, as in PG (the statement's
18192        // own WITH comes before INSERT).
18193        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18194            let select_stmt = if self.peek_is_with_kw() {
18195                self.advance();
18196                self.parse_nested_with_select()?
18197            } else {
18198                match self.parse_select_stmt()? {
18199                    Statement::Select(s) => s,
18200                    other => {
18201                        return Err(self.err(alloc::format!(
18202                            "expected SELECT after INSERT INTO ... target, got {other:?}"
18203                        )));
18204                    }
18205                }
18206            };
18207            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18208            let returning = self.parse_optional_returning()?;
18209            return Ok(Statement::Insert(InsertStatement {
18210                ctes: Vec::new(),
18211                table,
18212                alias: alias.clone(),
18213                columns,
18214                rows: Vec::new(),
18215                select_source: Some(Box::new(select_stmt)),
18216                on_conflict,
18217                returning,
18218                overriding,
18219                mysql_ignore: ignore,
18220            }));
18221        }
18222        if !matches!(self.peek(), Token::Values) {
18223            return Err(self.err(format!(
18224                "expected VALUES or SELECT after table name, got {:?}",
18225                self.peek()
18226            )));
18227        }
18228        self.advance();
18229        if !matches!(self.peek(), Token::LParen) {
18230            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18231        }
18232        let mut rows = Vec::new();
18233        loop {
18234            // Each iteration consumes one `(expr, expr, …)` tuple.
18235            if !matches!(self.peek(), Token::LParen) {
18236                return Err(self.err(format!(
18237                    "expected '(' for next VALUES tuple, got {:?}",
18238                    self.peek()
18239                )));
18240            }
18241            self.advance();
18242            let mut tuple = Vec::new();
18243            loop {
18244                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18245                // the column's declared default for that slot. Rides out as the
18246                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18247                // path uses; the INSERT executor resolves it per target column.
18248                if matches!(self.peek(), Token::Default) {
18249                    self.advance();
18250                    tuple.push(Expr::FunctionCall {
18251                        name: "__column_default".to_string(),
18252                        args: Vec::new(),
18253                    });
18254                } else {
18255                    tuple.push(self.parse_expr(0)?);
18256                }
18257                match self.peek() {
18258                    Token::Comma => {
18259                        self.advance();
18260                    }
18261                    Token::RParen => {
18262                        self.advance();
18263                        break;
18264                    }
18265                    other => {
18266                        return Err(self.err(format!(
18267                            "expected ',' or ')' in VALUES tuple, got {other:?}"
18268                        )));
18269                    }
18270                }
18271            }
18272            if tuple.is_empty() {
18273                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18274            }
18275            rows.push(tuple);
18276            // Continue with comma-separated tuples.
18277            if matches!(self.peek(), Token::Comma) {
18278                self.advance();
18279            } else {
18280                break;
18281            }
18282        }
18283        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18284        // to ON CONFLICT DO UPDATE with an empty conflict target
18285        // (the engine picks the table's first unique index, which
18286        // matches MySQL's any-unique-key behaviour for the common
18287        // single-key case). `VALUES(col)` in the assignments is
18288        // MySQL's spelling of EXCLUDED.col.
18289        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18290        let returning = self.parse_optional_returning()?;
18291        Ok(Statement::Insert(InsertStatement {
18292            ctes: Vec::new(),
18293            table,
18294            alias,
18295            columns,
18296            rows,
18297            select_source: None,
18298            on_conflict,
18299            returning,
18300            overriding,
18301            mysql_ignore: ignore,
18302        }))
18303    }
18304
18305    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18306    /// the incoming row's value — exactly PG's EXCLUDED.col.
18307    fn rewrite_mysql_values_refs(e: &mut Expr) {
18308        match e {
18309            Expr::FunctionCall { name, args }
18310                if name.eq_ignore_ascii_case("values")
18311                    && args.len() == 1
18312                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18313            {
18314                let Expr::Column(c) = &args[0] else {
18315                    unreachable!("guarded above");
18316                };
18317                *e = Expr::Column(crate::ast::ColumnName {
18318                    qualifier: Some("EXCLUDED".to_string()),
18319                    name: c.name.clone(),
18320                });
18321            }
18322            Expr::FunctionCall { args, .. } => {
18323                for a in args {
18324                    Self::rewrite_mysql_values_refs(a);
18325                }
18326            }
18327            Expr::Binary { lhs, rhs, .. } => {
18328                Self::rewrite_mysql_values_refs(lhs);
18329                Self::rewrite_mysql_values_refs(rhs);
18330            }
18331            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18332                Self::rewrite_mysql_values_refs(expr);
18333            }
18334            Expr::Case {
18335                operand,
18336                branches,
18337                else_branch,
18338            } => {
18339                if let Some(op) = operand {
18340                    Self::rewrite_mysql_values_refs(op);
18341                }
18342                for (w, t) in branches {
18343                    Self::rewrite_mysql_values_refs(w);
18344                    Self::rewrite_mysql_values_refs(t);
18345                }
18346                if let Some(el) = else_branch {
18347                    Self::rewrite_mysql_values_refs(el);
18348                }
18349            }
18350            _ => {}
18351        }
18352    }
18353
18354    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18355    /// clause sitting between the INSERT body and the trailing
18356    /// RETURNING. All keywords come in as bare idents; `ON` is
18357    /// a reserved Token though.
18358    fn parse_optional_on_conflict(
18359        &mut self,
18360    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18361        if !matches!(self.peek(), Token::On) {
18362            return Ok(None);
18363        }
18364        // Peek further: we want exactly "ON CONFLICT ...". If the
18365        // next ident isn't "conflict", let some other parser handle.
18366        let next_is_conflict = matches!(
18367            self.tokens.get(self.pos + 1),
18368            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18369        );
18370        if !next_is_conflict {
18371            return Ok(None);
18372        }
18373        self.advance(); // ON
18374        self.advance(); // CONFLICT
18375        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18376        // the constraint instead of listing columns (the pg_dump
18377        // form); the engine resolves it.
18378        let mut constraint_name: Option<String> = None;
18379        if matches!(self.peek(), Token::On) {
18380            self.advance(); // ON
18381            match self.advance() {
18382                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18383                }
18384                other => {
18385                    return Err(self.err(alloc::format!(
18386                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18387                    )));
18388                }
18389            }
18390            constraint_name = Some(self.expect_ident_like()?);
18391        }
18392        // Optional `(col [, col]*)` target list.
18393        let mut target_columns: Vec<String> = Vec::new();
18394        if matches!(self.peek(), Token::LParen) {
18395            self.advance();
18396            loop {
18397                target_columns.push(self.expect_ident_like()?);
18398                match self.peek() {
18399                    Token::Comma => {
18400                        self.advance();
18401                    }
18402                    Token::RParen => {
18403                        self.advance();
18404                        break;
18405                    }
18406                    other => {
18407                        return Err(self.err(alloc::format!(
18408                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18409                        )));
18410                    }
18411                }
18412            }
18413        }
18414        // v7.39 (round 240) — optional index predicate after the target
18415        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18416        // PARTIAL unique index; SPG's arbiters are full indexes, which
18417        // satisfy any predicate, so it is parsed and carried but not
18418        // consulted (recorded residual: partial-unique-index arbiters).
18419        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18420            self.advance();
18421            Some(self.parse_expr(0)?)
18422        } else {
18423            None
18424        };
18425        // Required `DO`.
18426        match self.advance() {
18427            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18428            other => {
18429                return Err(self.err(alloc::format!(
18430                    "expected DO after ON CONFLICT [(…)], got {other:?}"
18431                )));
18432            }
18433        }
18434        // Action: NOTHING | UPDATE SET …
18435        let action = match self.advance() {
18436            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18437                crate::ast::OnConflictAction::Nothing
18438            }
18439            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18440                self.parse_on_conflict_update_action()?
18441            }
18442            other => {
18443                return Err(self.err(alloc::format!(
18444                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18445                )));
18446            }
18447        };
18448        Ok(Some(crate::ast::OnConflictClause {
18449            target_columns,
18450            index_where,
18451            constraint_name,
18452            mysql_lowered: false,
18453            action,
18454        }))
18455    }
18456
18457    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18458    /// `SET col = expr [, …] [WHERE cond]`. Caller already
18459    /// consumed `UPDATE`.
18460    fn parse_on_conflict_update_action(
18461        &mut self,
18462    ) -> Result<crate::ast::OnConflictAction, ParseError> {
18463        // `SET`
18464        match self.advance() {
18465            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18466            other => {
18467                return Err(self.err(alloc::format!(
18468                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18469                )));
18470            }
18471        }
18472        let mut assignments: Vec<(String, Expr)> = Vec::new();
18473        loop {
18474            let col = self.expect_ident_like()?;
18475            if !matches!(self.peek(), Token::Eq) {
18476                return Err(self.err(alloc::format!(
18477                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18478                    self.peek()
18479                )));
18480            }
18481            self.advance();
18482            let value = self.parse_expr(0)?;
18483            assignments.push((col, value));
18484            if matches!(self.peek(), Token::Comma) {
18485                self.advance();
18486                continue;
18487            }
18488            break;
18489        }
18490        let where_ = if matches!(self.peek(), Token::Where) {
18491            self.advance();
18492            Some(self.parse_expr(0)?)
18493        } else {
18494            None
18495        };
18496        Ok(crate::ast::OnConflictAction::Update {
18497            assignments,
18498            where_,
18499        })
18500    }
18501
18502    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18503        let mut items = Vec::new();
18504        // v7.39 (round 341, V66) — PG's target list may be EMPTY
18505        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18506        // answers one zero-column row per row of t, and a bare `SELECT`
18507        // answers a single zero-column row. SPG required at least one
18508        // item, so both were syntax errors. Recognised by the token that
18509        // follows — nothing that can start an expression appears here.
18510        if self.select_list_is_empty_here() {
18511            return Ok(items);
18512        }
18513        loop {
18514            items.push(self.parse_select_item()?);
18515            if matches!(self.peek(), Token::Comma) {
18516                self.advance();
18517            } else {
18518                break;
18519            }
18520        }
18521        Ok(items)
18522    }
18523
18524    /// Is the target list empty at this point — i.e. does the next token
18525    /// end the SELECT's item list rather than start an item?
18526    fn select_list_is_empty_here(&self) -> bool {
18527        match self.peek() {
18528            Token::From
18529            | Token::Where
18530            | Token::Group
18531            | Token::Having
18532            | Token::Order
18533            | Token::Limit
18534            | Token::Offset
18535            | Token::Semicolon
18536            | Token::RParen
18537            | Token::Union
18538            | Token::Except
18539            | Token::Eof => true,
18540            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18541            // with unreserved keywords, so they arrive as plain idents.
18542            Token::Ident(s) => {
18543                s.eq_ignore_ascii_case("fetch")
18544                    || s.eq_ignore_ascii_case("window")
18545                    || s.eq_ignore_ascii_case("intersect")
18546            }
18547            _ => false,
18548        }
18549    }
18550
18551    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18552        if matches!(self.peek(), Token::Star) {
18553            self.advance();
18554            return Ok(SelectItem::Wildcard);
18555        }
18556        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18557        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18558        // choke on the `*` ("expected identifier, got Star"). The lookahead is
18559        // `<ident> . *` with nothing binding tighter.
18560        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18561            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18562                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18563            {
18564                self.advance(); // qualifier
18565                self.advance(); // .
18566                self.advance(); // *
18567                return Ok(SelectItem::QualifiedWildcard(q));
18568            }
18569        }
18570        let start_tok = self.pos;
18571        let expr = self.parse_expr(0)?;
18572        let end_tok = self.consumed_pos();
18573        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18574        // multi-column function returns into columns. Marked here and lowered in
18575        // `parse_bare_select`, where the FROM clause is in hand.
18576        if matches!(self.peek(), Token::Dot)
18577            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18578        {
18579            self.advance(); // .
18580            self.advance(); // *
18581            return Ok(SelectItem::Expr {
18582                expr: Expr::FunctionCall {
18583                    name: "__record_expand".to_string(),
18584                    args: alloc::vec![expr],
18585                },
18586                alias: None,
18587            });
18588        }
18589        let alias = match self.parse_optional_alias()? {
18590            Some(a) => Some(a),
18591            None => self.mysql_item_label(&expr, start_tok, end_tok),
18592        };
18593        Ok(SelectItem::Expr { expr, alias })
18594    }
18595
18596    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18597    /// carries no `AS`, filled in here so every downstream path reports it
18598    /// without knowing the rule. `None` leaves the item un-aliased, which is
18599    /// what a PG session always gets.
18600    ///
18601    /// Measured against MariaDB 11, three rules and no more:
18602    ///
18603    /// | item             | label      | why                          |
18604    /// |------------------|------------|------------------------------|
18605    /// | `lbl.a`          | `a`        | a column reports its name    |
18606    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
18607    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
18608    ///
18609    /// The third is why this lives in the parser at all: the label is the
18610    /// text the client WROTE, down to the spacing, so it cannot be printed
18611    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18612    ///
18613    /// Comments survive, and that is right: through a `mariadb` CLI both
18614    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
18615    /// CLIENT stripping the comment before it sends. Asked over the raw
18616    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18617    /// produces.
18618    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18619        if !self.mysql_dialect {
18620            return None;
18621        }
18622        match expr {
18623            // A column already reports its own name downstream; naming it
18624            // again here would only re-state the qualifier the label drops.
18625            Expr::Column(_) => None,
18626            Expr::Literal(Literal::String(v)) => Some(v.clone()),
18627            _ => self.source_span(start_tok, end_tok).map(str::to_string),
18628        }
18629    }
18630
18631    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18632    /// consumed VALUES keyword. Each row lowers to a constant SELECT
18633    /// with PG's default column1..columnN names; subsequent rows
18634    /// chain as UNION ALL peers. Shared by the FROM-position
18635    /// `( VALUES … )` arm and the top-level bare VALUES statement.
18636    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18637        let mut row_selects: Vec<SelectStatement> = Vec::new();
18638        loop {
18639            if !matches!(self.peek(), Token::LParen) {
18640                return Err(self.err(alloc::format!(
18641                    "expected '(' to start a VALUES row, got {:?}",
18642                    self.peek()
18643                )));
18644            }
18645            self.advance(); // (
18646            let mut items: Vec<SelectItem> = Vec::new();
18647            loop {
18648                let expr = self.parse_expr(0)?;
18649                items.push(SelectItem::Expr {
18650                    expr,
18651                    alias: Some(alloc::format!("column{}", items.len() + 1)),
18652                });
18653                match self.peek() {
18654                    Token::Comma => {
18655                        self.advance();
18656                    }
18657                    Token::RParen => break,
18658                    other => {
18659                        return Err(self.err(alloc::format!(
18660                            "expected ',' or ')' in VALUES row, got {other:?}"
18661                        )));
18662                    }
18663                }
18664            }
18665            self.advance(); // )
18666            row_selects.push(SelectStatement {
18667                locking: None,
18668                ctes: Vec::new(),
18669                distinct: false,
18670                distinct_on: Vec::new(),
18671                items,
18672                from: None,
18673                where_: None,
18674                group_by: None,
18675                group_by_all: false,
18676                having: None,
18677                unions: Vec::new(),
18678                order_by: Vec::new(),
18679                limit: None,
18680                offset: None,
18681                limit_with_ties: false,
18682                window_check_exprs: Vec::new(),
18683            });
18684            if matches!(self.peek(), Token::Comma) {
18685                self.advance();
18686                continue;
18687            }
18688            break;
18689        }
18690        let mut head = row_selects.remove(0);
18691        head.unions = row_selects
18692            .into_iter()
18693            .map(|s| (UnionKind::All, s))
18694            .collect();
18695        Ok(head)
18696    }
18697
18698    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18699        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18700        // children. It was read as a table NAMED `only`, so the query
18701        // failed on `relation "only" does not exist`.
18702        //
18703        // v7.39 (round 644) — and it is no longer a no-op. Round 621
18704        // absorbed the keyword, reasoning that SPG's children are
18705        // separate relations a plain scan does not descend into, so ONLY
18706        // already described the scan. That stopped being true when a
18707        // partition parent started unioning its children: measured,
18708        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18709        // where PG answers 0. The flag is carried now.
18710        let mut only = false;
18711        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18712            && matches!(
18713                self.tokens.get(self.pos + 1),
18714                Some(Token::Ident(_) | Token::QuotedIdent(_))
18715            )
18716        {
18717            only = true;
18718            self.advance();
18719        }
18720        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18721        // for these SRFs the keyword is noise at parse time: the
18722        // join executor already substitutes outer-column references
18723        // into unnest_expr / generate_series_args per outer row
18724        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18725        // licences the correlation even without the keyword. Absorb
18726        // it and fall through to the SRF arms below.
18727        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18728        // just the four builtin SRFs: a user set-returning function on a JOIN's
18729        // right side is the whole point of LATERAL. The keyword stays noise at
18730        // parse time — the join executor substitutes the outer row into the
18731        // call's arguments per outer row.
18732        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18733            && matches!(
18734                self.tokens.get(self.pos + 1),
18735                // The json_each family has its OWN `LATERAL …` arm below, which
18736                // needs to see the keyword — absorbing it here would send those
18737                // calls down the generic table-function channel instead.
18738                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18739            )
18740            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18741        {
18742            self.advance(); // LATERAL
18743        }
18744        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18745        // set-returning function whose argument may reference a
18746        // preceding FROM item. We rewrite this to
18747        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18748        // AS __srf__) AS <alias>` so the existing LATERAL subquery
18749        // executor handles per-outer-row evaluation and the
18750        // SRF-primary jsonb_each_text path handles the inner
18751        // materialisation. Sentori 0067 backfill is the dogfood
18752        // shape.
18753        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18754            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18755            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18756        {
18757            self.advance(); // LATERAL
18758            let each_fn = match self.peek() {
18759                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18760                _ => unreachable!(),
18761            };
18762            self.advance(); // jsonb_each[_text] / json_each[_text]
18763            self.advance(); // (
18764            let arg = self.parse_expr(0)?;
18765            if !matches!(self.peek(), Token::RParen) {
18766                return Err(self.err(alloc::format!(
18767                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18768                    self.peek()
18769                )));
18770            }
18771            self.advance();
18772            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18773            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18774            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18775            //               FROM jsonb_each_text(<arg>) AS __srf__
18776            // PG's `AS kv(key, value)` column-alias list maps
18777            // positions to names; default to (key, value) when
18778            // omitted (matching the SRF's natural column names).
18779            let srf_alias = "__srf__".to_string();
18780            let key_alias = column_aliases
18781                .first()
18782                .cloned()
18783                .unwrap_or_else(|| "key".to_string());
18784            let value_alias = column_aliases
18785                .get(1)
18786                .cloned()
18787                .unwrap_or_else(|| "value".to_string());
18788            let inner_select = crate::ast::SelectStatement {
18789                locking: None,
18790                ctes: Vec::new(),
18791                distinct: false,
18792                distinct_on: Vec::new(),
18793                items: alloc::vec![
18794                    crate::ast::SelectItem::Expr {
18795                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18796                            qualifier: Some(srf_alias.clone()),
18797                            name: "key".to_string(),
18798                        }),
18799                        alias: Some(key_alias),
18800                    },
18801                    crate::ast::SelectItem::Expr {
18802                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18803                            qualifier: Some(srf_alias.clone()),
18804                            name: "value".to_string(),
18805                        }),
18806                        alias: Some(value_alias),
18807                    },
18808                ],
18809                from: Some(crate::ast::FromClause {
18810                    primary: TableRef {
18811                        name: srf_alias.clone(),
18812                        alias: Some(srf_alias.clone()),
18813                        only: false,
18814                        as_of_segment: None,
18815                        unnest_expr: None,
18816                        unnest_column_aliases: Vec::new(),
18817                        with_ordinality: false,
18818                        generate_series_args: None,
18819                        lateral_subquery: None,
18820                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18821                        table_fn_call: None,
18822                        rows_from: None,
18823                        json_table: None,
18824                        scalar_fn_item: false,
18825                    },
18826                    joins: Vec::new(),
18827                }),
18828                where_: None,
18829                group_by: None,
18830                group_by_all: false,
18831                having: None,
18832                unions: Vec::new(),
18833                order_by: Vec::new(),
18834                limit: None,
18835                offset: None,
18836                limit_with_ties: false,
18837                window_check_exprs: Vec::new(),
18838            };
18839            return Ok(TableRef {
18840                name: alias.clone(),
18841                alias: Some(alias),
18842                only: false,
18843                as_of_segment: None,
18844                unnest_expr: None,
18845                unnest_column_aliases: Vec::new(),
18846                with_ordinality: false,
18847                generate_series_args: None,
18848                lateral_subquery: Some(Box::new(inner_select)),
18849                jsonb_each_text_arg: None,
18850                table_fn_call: None,
18851                rows_from: None,
18852                json_table: None,
18853                scalar_fn_item: false,
18854            });
18855        }
18856        // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18857        // without an explicit `LATERAL` keyword is the same shape
18858        // PG accepts (SRF naturally licences lateral correlation).
18859        // We mirror the LATERAL rewrite when the argument syntactic-
18860        // ally references an outer column (Column { qualifier:
18861        // Some(_), … }). For simplicity we apply the rewrite
18862        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18863        // in the FROM-list — caller-side join parsing positions
18864        // this peek correctly.
18865        // (Implementation note: detection lives below; the LATERAL
18866        // branch above already covers the explicit form; the bare
18867        // form falls through to the plain SRF arm and the engine
18868        // treats it as a constant-arg SRF if no outer reference is
18869        // present.)
18870        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18871        // table. Detect at the head so it claims precedence over
18872        // every other table-ref shape (unnest / generate_series /
18873        // bare ident); the lateral subquery itself follows the
18874        // regular SELECT grammar.
18875        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18876        // t(cols)`. Each row lowers to a constant SELECT with PG's
18877        // default column1..columnN names; subsequent rows chain as
18878        // UNION ALL peers. The result rides the derived-table
18879        // lateral_subquery channel — zero executor work.
18880        if matches!(self.peek(), Token::LParen)
18881            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18882        {
18883            self.advance(); // (
18884            self.advance(); // VALUES
18885            let head = self.parse_values_rows_body()?;
18886            if !matches!(self.peek(), Token::RParen) {
18887                return Err(self.err(alloc::format!(
18888                    "expected ')' after VALUES list, got {:?}",
18889                    self.peek()
18890                )));
18891            }
18892            self.advance();
18893            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18894            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18895            return Ok(TableRef {
18896                name,
18897                alias: alias_ident,
18898                only: false,
18899                as_of_segment: None,
18900                unnest_expr: None,
18901                unnest_column_aliases: column_aliases,
18902                with_ordinality: false,
18903                generate_series_args: None,
18904                lateral_subquery: Some(Box::new(head)),
18905                jsonb_each_text_arg: None,
18906                table_fn_call: None,
18907                rows_from: None,
18908                json_table: None,
18909                scalar_fn_item: false,
18910            });
18911        }
18912        // v7.37.17 (17.6 siblings) — plain derived table:
18913        // `FROM ( SELECT … ) [AS] alias`. Rides the same
18914        // lateral_subquery channel the explicit LATERAL form uses —
18915        // an uncorrelated inner SELECT executes identically. The
18916        // inner parse carries UNION tails (they live on
18917        // SelectStatement.unions).
18918        // v7.37 D.20 — the derived-table inner may itself be a
18919        // parenthesized set-operation group (`FROM ((SELECT…) UNION
18920        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18921        // bare `(SELECT …)`. parse_one_statement already routes a leading
18922        // `(` set-op group (its LParen arm) and a leading WITH
18923        // (parse_with_cte_then_select), so widen the second-token gate to
18924        // Select | LParen | WITH.
18925        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18926        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18927        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18928        // has existed since the shorthand landed and `parse_bare_select`
18929        // already routes it ("valid anywhere a SELECT head is"); what was
18930        // missing is this second-token gate, and the CTE body's dispatch
18931        // below. Round 868 found both by putting the shorthand in a
18932        // subquery — the top-level forms had been the only ones tested.
18933        if matches!(self.peek(), Token::LParen)
18934            && (matches!(
18935                self.tokens.get(self.pos + 1),
18936                Some(Token::Select | Token::LParen | Token::Table)
18937            ) || matches!(self.tokens.get(self.pos + 1),
18938                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18939        {
18940            self.advance(); // (
18941            let inner = match self.parse_one_statement()? {
18942                Statement::Select(s) => s,
18943                other => {
18944                    return Err(self.err(alloc::format!(
18945                        "expected SELECT inside derived table ( … ), got {other:?}"
18946                    )));
18947                }
18948            };
18949            if !matches!(self.peek(), Token::RParen) {
18950                return Err(self.err(alloc::format!(
18951                    "expected ')' after derived-table subquery, got {:?}",
18952                    self.peek()
18953                )));
18954            }
18955            self.advance();
18956            // `AS t(a, b)` column-alias list rides the
18957            // unnest_column_aliases field (same positional-rename
18958            // contract the unnest SRFs use).
18959            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18960            let name = alias_ident
18961                .clone()
18962                .unwrap_or_else(|| "subquery".to_string());
18963            return Ok(TableRef {
18964                name,
18965                alias: alias_ident,
18966                only: false,
18967                as_of_segment: None,
18968                unnest_expr: None,
18969                unnest_column_aliases: column_aliases,
18970                with_ordinality: false,
18971                generate_series_args: None,
18972                lateral_subquery: Some(Box::new(inner)),
18973                jsonb_each_text_arg: None,
18974                table_fn_call: None,
18975                rows_from: None,
18976                json_table: None,
18977                scalar_fn_item: false,
18978            });
18979        }
18980        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18981            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18982        {
18983            self.advance(); // LATERAL
18984            self.advance(); // (
18985            // Parse the inner SELECT.
18986            let inner = match self.parse_one_statement()? {
18987                Statement::Select(s) => s,
18988                other => {
18989                    return Err(self.err(alloc::format!(
18990                        "expected SELECT inside LATERAL ( … ), got {other:?}"
18991                    )));
18992                }
18993            };
18994            if !matches!(self.peek(), Token::RParen) {
18995                return Err(self.err(alloc::format!(
18996                    "expected ')' after LATERAL subquery, got {:?}",
18997                    self.peek()
18998                )));
18999            }
19000            self.advance();
19001            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19002            // `(VALUES …) t(g)` derived table round-trips through view-body
19003            // Display, which renders on the lateral_subquery channel).
19004            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19005            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19006            return Ok(TableRef {
19007                name,
19008                alias: alias_ident,
19009                only: false,
19010                as_of_segment: None,
19011                unnest_expr: None,
19012                unnest_column_aliases: column_aliases,
19013                with_ordinality: false,
19014                generate_series_args: None,
19015                lateral_subquery: Some(Box::new(inner)),
19016                jsonb_each_text_arg: None,
19017                table_fn_call: None,
19018                rows_from: None,
19019                json_table: None,
19020                scalar_fn_item: false,
19021            });
19022        }
19023        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19024        // function as a FROM item. Emits one row per (key, value)
19025        // pair in the JSONB object argument as TEXT columns. May
19026        // be wrapped in CROSS JOIN LATERAL when the argument
19027        // references a preceding FROM item (sentori migration
19028        // 0067 backfill shape: `CROSS JOIN LATERAL
19029        // jsonb_each_text(t.json_col) AS kv(key, value)`).
19030        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19031            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19032        {
19033            let each_fn = match self.peek() {
19034                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19035                _ => unreachable!(),
19036            };
19037            self.advance(); // jsonb_each[_text] / json_each[_text]
19038            self.advance(); // (
19039            let arg = self.parse_expr(0)?;
19040            if !matches!(self.peek(), Token::RParen) {
19041                return Err(self.err(alloc::format!(
19042                    "expected ')' after {each_fn}() argument, got {:?}",
19043                    self.peek()
19044                )));
19045            }
19046            self.advance();
19047            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19048            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19049            return Ok(TableRef {
19050                name,
19051                alias: alias_ident,
19052                only: false,
19053                as_of_segment: None,
19054                unnest_expr: None,
19055                // `AS t(k, v)` renames key/value positionally, same as the
19056                // LATERAL-position form already does.
19057                unnest_column_aliases: column_aliases,
19058                with_ordinality: false,
19059                generate_series_args: None,
19060                lateral_subquery: None,
19061                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19062                table_fn_call: None,
19063                rows_from: None,
19064                json_table: None,
19065                scalar_fn_item: false,
19066            });
19067        }
19068        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19069        // (+ json_ variants) — record-returning JSON functions with a
19070        // column-definition list. Desugar to a derived table that
19071        // projects each declared column from the JSON via `->>` + a cast,
19072        // over `jsonb_array_elements(J)` for the *set (per-element) form.
19073        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19074            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19075        {
19076            return self.parse_json_to_record_from();
19077        }
19078        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19079        // row is a text[] of capture groups, so it cannot desugar to unnest
19080        // (that would flatten the array). Wrap it as a derived table
19081        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19082        // SRF path already emits one text[] row per match. PG names the column
19083        // `regexp_matches`; an `AS a(col)` alias overrides it.
19084        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19085                if s.eq_ignore_ascii_case("regexp_matches"))
19086            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19087        {
19088            self.advance(); // fn name
19089            self.advance(); // (
19090            let mut fn_args: Vec<Expr> = Vec::new();
19091            loop {
19092                fn_args.push(self.parse_expr(0)?);
19093                if matches!(self.peek(), Token::Comma) {
19094                    self.advance();
19095                    continue;
19096                }
19097                break;
19098            }
19099            if !matches!(self.peek(), Token::RParen) {
19100                return Err(self.err(alloc::format!(
19101                    "expected ')' after regexp_matches() arguments, got {:?}",
19102                    self.peek()
19103                )));
19104            }
19105            self.advance();
19106            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19107            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19108            // it, so it died on the `with` token while every other table function
19109            // accepted it.
19110            let with_ordinality = self.absorb_with_ordinality();
19111            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19112            let table_alias = alias_ident
19113                .clone()
19114                .unwrap_or_else(|| "regexp_matches".to_string());
19115            // PG names a single-column function's output column after the ALIAS
19116            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19117            // `m` reads as that column and not as a whole-row composite. Naming
19118            // it after the function regardless made `SELECT m[1] FROM … AS m`
19119            // subscript a record.
19120            let col_name = column_aliases
19121                .first()
19122                .cloned()
19123                .or_else(|| alias_ident.clone())
19124                .unwrap_or_else(|| "regexp_matches".to_string());
19125            let inner = crate::ast::SelectStatement {
19126                locking: None,
19127                ctes: Vec::new(),
19128                distinct: false,
19129                distinct_on: Vec::new(),
19130                items: alloc::vec![SelectItem::Expr {
19131                    expr: Expr::FunctionCall {
19132                        name: "regexp_matches".to_string(),
19133                        args: fn_args,
19134                    },
19135                    alias: Some(col_name),
19136                }],
19137                from: None,
19138                where_: None,
19139                group_by: None,
19140                group_by_all: false,
19141                having: None,
19142                unions: Vec::new(),
19143                order_by: Vec::new(),
19144                limit: None,
19145                offset: None,
19146                limit_with_ties: false,
19147                window_check_exprs: Vec::new(),
19148            };
19149            return Ok(TableRef {
19150                name: table_alias.clone(),
19151                alias: Some(table_alias),
19152                only: false,
19153                as_of_segment: None,
19154                unnest_expr: None,
19155                unnest_column_aliases: column_aliases,
19156                with_ordinality,
19157                generate_series_args: None,
19158                lateral_subquery: Some(Box::new(inner)),
19159                jsonb_each_text_arg: None,
19160                table_fn_call: None,
19161                rows_from: None,
19162                json_table: None,
19163                // regexp_matches returns text[], a base type: `SELECT m FROM
19164                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19165                scalar_fn_item: true,
19166            });
19167        }
19168        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19169        // / json_ variants as a FROM item. Rewritten into
19170        // `unnest(<same fn>(<expr>))`: the scalar form returns the
19171        // elements as a TEXT array, and the existing unnest SRF path
19172        // materialises one row per element. PG's natural column name
19173        // is `value`; an `AS a(col)` column-alias list overrides it.
19174        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19175                if s.eq_ignore_ascii_case("jsonb_array_elements")
19176                    || s.eq_ignore_ascii_case("json_array_elements")
19177                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19178                    || s.eq_ignore_ascii_case("json_array_elements_text")
19179                    || s.eq_ignore_ascii_case("jsonb_object_keys")
19180                    || s.eq_ignore_ascii_case("json_object_keys")
19181                    || s.eq_ignore_ascii_case("jsonb_path_query")
19182                    || s.eq_ignore_ascii_case("json_path_query")
19183                    || s.eq_ignore_ascii_case("generate_subscripts")
19184                    || s.eq_ignore_ascii_case("string_to_table")
19185                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
19186            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19187        {
19188            let fn_name = match self.peek() {
19189                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19190                _ => unreachable!(),
19191            };
19192            self.advance(); // fn name
19193            self.advance(); // (
19194            let mut fn_args: Vec<Expr> = Vec::new();
19195            loop {
19196                fn_args.push(self.parse_expr(0)?);
19197                if matches!(self.peek(), Token::Comma) {
19198                    self.advance();
19199                    continue;
19200                }
19201                break;
19202            }
19203            if !matches!(self.peek(), Token::RParen) {
19204                return Err(self.err(alloc::format!(
19205                    "expected ')' after {fn_name}() arguments, got {:?}",
19206                    self.peek()
19207                )));
19208            }
19209            self.advance();
19210            let with_ordinality = self.absorb_with_ordinality();
19211            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19212            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19213            // PG's natural column name: the array-elements SRFs
19214            // declare an OUT parameter `value`; jsonb_object_keys
19215            // and generate_subscripts have none, so the column is
19216            // named after the function. A bare table alias on a
19217            // single-column SRF renames the column too (PG: `FROM
19218            // generate_subscripts(a, 1) AS s` projects column s) —
19219            // except for the OUT-parameter SRFs, whose column stays
19220            // `value` under a bare alias.
19221            let natural_col = if fn_name.ends_with("_array_elements")
19222                || fn_name.ends_with("_array_elements_text")
19223            {
19224                "value".to_string()
19225            } else {
19226                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19227            };
19228            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19229            // Keep any further entries — the second names the
19230            // ordinality column under WITH ORDINALITY.
19231            srf_cols.extend(column_aliases.into_iter().skip(1));
19232            // The *_to_table SRFs are row-streams over the existing
19233            // *_to_array scalars — map the call target; the display
19234            // name (alias / column defaults) keeps the SRF spelling.
19235            let call_name = match fn_name.as_str() {
19236                "string_to_table" => "string_to_array".to_string(),
19237                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19238                _ => fn_name,
19239            };
19240            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19241            // preceding FROM item (bare or qualified column) is correlated;
19242            // route it through the per-outer-row lateral channel.
19243            let expr = crate::ast::Expr::FunctionCall {
19244                name: call_name,
19245                args: fn_args,
19246            };
19247            let correlated = Self::expr_has_any_column(&expr);
19248            let tref = TableRef {
19249                name,
19250                alias: alias_ident,
19251                only: false,
19252                as_of_segment: None,
19253                unnest_expr: Some(Box::new(expr)),
19254                unnest_column_aliases: srf_cols,
19255                with_ordinality,
19256                generate_series_args: None,
19257                lateral_subquery: None,
19258                jsonb_each_text_arg: None,
19259                table_fn_call: None,
19260                rows_from: None,
19261                json_table: None,
19262                // Each of these returns a BASE type (jsonb / text / int), so the item's
19263                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19264                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19265                scalar_fn_item: !with_ordinality,
19266            };
19267            return Ok(if correlated {
19268                Self::wrap_correlated_srf(tref)
19269            } else {
19270                tref
19271            });
19272        }
19273        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19274        // explicit parallel-zip syntax. Each entry lowers to its
19275        // array-returning scalar form (unnest(x) → x itself; the
19276        // FROM-SRF rewrite family → their scalar array calls) and
19277        // the list rides the multi-arg unnest zip channel:
19278        // NULL-padded to the longest, WITH ORDINALITY appends the
19279        // counter. generate_series has no scalar array form and
19280        // errors honestly.
19281        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19282            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19283            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19284        {
19285            self.advance(); // ROWS
19286            self.advance(); // FROM
19287            self.advance(); // (
19288            let mut entries: Vec<Expr> = Vec::new();
19289            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19290            // Used only when some entry has no array form.
19291            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19292            loop {
19293                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19294                if !matches!(self.peek(), Token::LParen) {
19295                    return Err(self.err(alloc::format!(
19296                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19297                        self.peek()
19298                    )));
19299                }
19300                self.advance();
19301                let mut fn_args: Vec<Expr> = Vec::new();
19302                if !matches!(self.peek(), Token::RParen) {
19303                    loop {
19304                        fn_args.push(self.parse_expr(0)?);
19305                        if matches!(self.peek(), Token::Comma) {
19306                            self.advance();
19307                            continue;
19308                        }
19309                        break;
19310                    }
19311                }
19312                if !matches!(self.peek(), Token::RParen) {
19313                    return Err(self.err(alloc::format!(
19314                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19315                        self.peek()
19316                    )));
19317                }
19318                self.advance();
19319                let entry = match fn_name.as_str() {
19320                    "unnest" => {
19321                        if fn_args.len() != 1 {
19322                            return Err(
19323                                self.err("unnest inside ROWS FROM takes exactly one array".into())
19324                            );
19325                        }
19326                        fn_args.pop().expect("len checked")
19327                    }
19328                    "jsonb_array_elements"
19329                    | "json_array_elements"
19330                    | "jsonb_array_elements_text"
19331                    | "json_array_elements_text"
19332                    | "jsonb_object_keys"
19333                    | "json_object_keys"
19334                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19335                        name: fn_name,
19336                        args: fn_args,
19337                    },
19338                    "string_to_table" => crate::ast::Expr::FunctionCall {
19339                        name: "string_to_array".to_string(),
19340                        args: fn_args,
19341                    },
19342                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19343                        name: "regexp_split_to_array".to_string(),
19344                        args: fn_args,
19345                    },
19346                    // v7.39 (read01 round 74) — an SRF with no array form
19347                    // (`generate_series`, a user `RETURNS SETOF` function) has no
19348                    // scalar expression to zip, so the WHOLE list switches to the
19349                    // rows_from channel, which runs each function and zips the
19350                    // rows themselves. The all-array case keeps the old lowering:
19351                    // it is well-trodden and this must not disturb it.
19352                    _ => {
19353                        generic.push((fn_name, fn_args));
19354                        if matches!(self.peek(), Token::Comma) {
19355                            self.advance();
19356                            continue;
19357                        }
19358                        break;
19359                    }
19360                };
19361                generic.push((
19362                    // The array-able entries carry their lowered expr along, so a
19363                    // MIXED list still works: the engine sees the scalar array
19364                    // form and unnests it.
19365                    "__array".to_string(),
19366                    alloc::vec![entry.clone()],
19367                ));
19368                entries.push(entry);
19369                if matches!(self.peek(), Token::Comma) {
19370                    self.advance();
19371                    continue;
19372                }
19373                break;
19374            }
19375            if !matches!(self.peek(), Token::RParen) {
19376                return Err(self.err(alloc::format!(
19377                    "expected ')' to close ROWS FROM, got {:?}",
19378                    self.peek()
19379                )));
19380            }
19381            self.advance();
19382            let with_ordinality = self.absorb_with_ordinality();
19383            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19384            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19385            // v7.39 (read01 round 74) — some entry had no array form, so the whole
19386            // list rides the generic channel.
19387            if generic.iter().any(|(n, _)| n != "__array") {
19388                let correlated = generic
19389                    .iter()
19390                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19391                let tref = TableRef {
19392                    name,
19393                    alias: alias_ident,
19394                    only: false,
19395                    as_of_segment: None,
19396                    unnest_expr: None,
19397                    unnest_column_aliases,
19398                    with_ordinality,
19399                    generate_series_args: None,
19400                    lateral_subquery: None,
19401                    jsonb_each_text_arg: None,
19402                    table_fn_call: None,
19403                    rows_from: Some(generic),
19404                    json_table: None,
19405                    scalar_fn_item: false,
19406                };
19407                return Ok(if correlated {
19408                    Self::wrap_correlated_srf(tref)
19409                } else {
19410                    tref
19411                });
19412            }
19413            let correlated = entries.iter().any(Self::expr_has_any_column);
19414            let expr = if entries.len() == 1 {
19415                entries.pop().expect("len checked")
19416            } else {
19417                crate::ast::Expr::FunctionCall {
19418                    name: "__unnest_zip".to_string(),
19419                    args: entries,
19420                }
19421            };
19422            let tref = TableRef {
19423                name,
19424                alias: alias_ident,
19425                only: false,
19426                as_of_segment: None,
19427                unnest_expr: Some(Box::new(expr)),
19428                unnest_column_aliases,
19429                with_ordinality,
19430                generate_series_args: None,
19431                lateral_subquery: None,
19432                jsonb_each_text_arg: None,
19433                table_fn_call: None,
19434                rows_from: None,
19435                json_table: None,
19436                scalar_fn_item: false,
19437            };
19438            return Ok(if correlated {
19439                Self::wrap_correlated_srf(tref)
19440            } else {
19441                tref
19442            });
19443        }
19444        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19445        // source. Detect at the head before the bare-ident fallback;
19446        // unnest is not a reserved token.
19447        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19448            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19449        {
19450            self.advance(); // unnest
19451            self.advance(); // (
19452            let mut srf_args = alloc::vec![self.parse_expr(0)?];
19453            while matches!(self.peek(), Token::Comma) {
19454                self.advance();
19455                srf_args.push(self.parse_expr(0)?);
19456            }
19457            if !matches!(self.peek(), Token::RParen) {
19458                return Err(self.err(alloc::format!(
19459                    "expected ')' after unnest() argument, got {:?}",
19460                    self.peek()
19461                )));
19462            }
19463            self.advance();
19464            // Multi-arg unnest(a, b, …) zips the arrays in
19465            // parallel, NULL-padding to the longest (PG's ROWS
19466            // FROM shorthand). Lower onto the unnest channel as an
19467            // internal marker call the executors unpack.
19468            let expr = if srf_args.len() == 1 {
19469                srf_args.pop().expect("len checked")
19470            } else {
19471                crate::ast::Expr::FunctionCall {
19472                    name: "__unnest_zip".to_string(),
19473                    args: srf_args,
19474                }
19475            };
19476            let with_ordinality = self.absorb_with_ordinality();
19477            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19478            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19479            let correlated = Self::expr_has_any_column(&expr);
19480            let tref = TableRef {
19481                name,
19482                alias: alias_ident,
19483                only: false,
19484                as_of_segment: None,
19485                unnest_expr: Some(Box::new(expr)),
19486                unnest_column_aliases,
19487                with_ordinality,
19488                generate_series_args: None,
19489                lateral_subquery: None,
19490                jsonb_each_text_arg: None,
19491                table_fn_call: None,
19492                rows_from: None,
19493                json_table: None,
19494                scalar_fn_item: false,
19495            };
19496            return Ok(if correlated {
19497                Self::wrap_correlated_srf(tref)
19498            } else {
19499                tref
19500            });
19501        }
19502        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19503        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19504        // generic table-fn arg parser can't read), so it is intercepted
19505        // here BEFORE the generic dispatch. The doc expr may reference
19506        // outer columns (implicit LATERAL) — same correlated-wrap rule.
19507        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19508                if s.eq_ignore_ascii_case("json_table"))
19509            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19510        {
19511            let tref = self.parse_json_table_ref()?;
19512            let correlated = tref
19513                .json_table
19514                .as_deref()
19515                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19516            return Ok(if correlated {
19517                Self::wrap_correlated_srf(tref)
19518            } else {
19519                tref
19520            });
19521        }
19522        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19523        // functions dispatched by name (`pg_partition_tree('t')`,
19524        // `pg_partition_ancestors('t')`). Same head-detection shape as
19525        // unnest; the engine executor owns the row shape per function.
19526        // v7.39 (read01 round 65) — and a USER function in FROM position
19527        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19528        // (generate_series / unnest / the json_each family) keep it — their arms
19529        // sit further down, so they are excluded here by name rather than by
19530        // ordering. Anything else that is an ident followed by `(` is a table
19531        // function; the engine executor decides whether it is a builtin, a
19532        // set-returning user function, or an error.
19533        // 7.38.1 S5.1 — pg_dump spells its table functions
19534        // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
19535        // strip the pg_catalog prefix here so the same head-detection
19536        // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
19537        // meaning.
19538        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
19539            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19540            && matches!(
19541                self.tokens.get(self.pos + 2),
19542                Some(Token::Ident(_) | Token::QuotedIdent(_))
19543            )
19544            && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
19545        {
19546            self.advance(); // pg_catalog
19547            self.advance(); // .
19548        }
19549        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19550                if !s.eq_ignore_ascii_case("generate_series")
19551                    && !s.eq_ignore_ascii_case("unnest")
19552                    && !is_json_each_name(s))
19553            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19554        {
19555            // Body out-of-line — this parse sits on the FROM/subquery
19556            // recursion chain (debug frame-cliff discipline).
19557            // v7.39 (read01 round 69) — a call whose arguments reference an outer
19558            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19559            // outer row, so it rides the lateral channel. Same rule the unnest
19560            // arm uses.
19561            let tref = self.parse_table_fn_ref()?;
19562            let correlated = tref
19563                .table_fn_call
19564                .as_deref()
19565                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19566            return Ok(if correlated {
19567                Self::wrap_correlated_srf(tref)
19568            } else {
19569                tref
19570            });
19571        }
19572        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19573        // [, step])` set-returning source. Same shape as unnest:
19574        // detect at the head, parse the comma-separated arg list,
19575        // dispatch downstream through the engine's set-returning
19576        // path. Supports integer triplets (mailrs's `WITH row_no AS
19577        // (SELECT * FROM generate_series(1, N))` pattern) and
19578        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19579        // date-range iteration pattern, which pre-3.10 had no
19580        // direct equivalent in SPG).
19581        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19582            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19583        {
19584            self.advance(); // generate_series
19585            self.advance(); // (
19586            let mut args: Vec<Expr> = Vec::new();
19587            loop {
19588                args.push(self.parse_expr(0)?);
19589                if matches!(self.peek(), Token::Comma) {
19590                    self.advance();
19591                    continue;
19592                }
19593                break;
19594            }
19595            if !matches!(self.peek(), Token::RParen) {
19596                return Err(self.err(alloc::format!(
19597                    "expected ')' after generate_series() arguments, got {:?}",
19598                    self.peek()
19599                )));
19600            }
19601            self.advance();
19602            if args.len() < 2 || args.len() > 3 {
19603                return Err(self.err(alloc::format!(
19604                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19605                    args.len()
19606                )));
19607            }
19608            let with_ordinality = self.absorb_with_ordinality();
19609            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19610            let name = alias_ident
19611                .clone()
19612                .unwrap_or_else(|| "generate_series".to_string());
19613            let correlated = args.iter().any(Self::expr_has_any_column);
19614            let tref = TableRef {
19615                name,
19616                alias: alias_ident,
19617                only: false,
19618                as_of_segment: None,
19619                unnest_expr: None,
19620                unnest_column_aliases: column_aliases,
19621                with_ordinality,
19622                generate_series_args: Some(args),
19623                lateral_subquery: None,
19624                jsonb_each_text_arg: None,
19625                table_fn_call: None,
19626                rows_from: None,
19627                json_table: None,
19628                scalar_fn_item: false,
19629            };
19630            return Ok(if correlated {
19631                Self::wrap_correlated_srf(tref)
19632            } else {
19633                tref
19634            });
19635        }
19636        // v7.16.2 — preserve information_schema / pg_catalog
19637        // qualifiers (mailrs round-10 A.3). The generic
19638        // `expect_ident_like` strip silently drops the schema;
19639        // we want the engine to recognise these PG meta tables
19640        // and synthesise rows from the live catalog. Produce a
19641        // synthetic name (`__spg_info_columns` etc.) so the
19642        // engine's SELECT-side router can dispatch without
19643        // clashing with any user-defined `columns` table.
19644        let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
19645            (synth, Some(orig))
19646        } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
19647            (synth, Some(orig))
19648        } else {
19649            (self.expect_ident_like()?, None)
19650        };
19651        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19652        // time-travel clause. Parse BEFORE the alias so the
19653        // alias can still ride at the tail (`tbl AS OF SEGMENT
19654        // '5' alias`). `AS` is a reserved keyword token, while
19655        // `OF` and `SEGMENT` are bare idents.
19656        let as_of_segment = if matches!(self.peek(), Token::As)
19657            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19658        {
19659            self.advance(); // AS
19660            self.advance(); // OF
19661            let kw = match self.peek().clone() {
19662                Token::Ident(s) | Token::QuotedIdent(s) => s,
19663                other => {
19664                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19665                }
19666            };
19667            if !kw.eq_ignore_ascii_case("segment") {
19668                return Err(self.err(format!(
19669                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19670                )));
19671            }
19672            self.advance();
19673            // Segment id literal — accept either a string or
19674            // integer for operator ergonomics.
19675            let id = match self.advance() {
19676                Token::String(s) => s
19677                    .parse::<u32>()
19678                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19679                Token::Integer(n) => u32::try_from(n)
19680                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19681                other => {
19682                    return Err(self.err(format!(
19683                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
19684                    )));
19685                }
19686            };
19687            Some(id)
19688        } else {
19689            None
19690        };
19691        // TABLESAMPLE is not a reserved token — keep the bare-ident
19692        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19693        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19694        {
19695            None
19696        } else {
19697            self.parse_optional_alias()?
19698        };
19699        // r1052 — a catalog name rewritten to its synthetic form keeps
19700        // the WRITTEN name as the relation's alias, so `pg_cast.oid`
19701        // still binds after `pg_cast` became `__spg_pg_cast`. PG
19702        // semantics: the visible name of `pg_catalog.pg_cast` IS
19703        // `pg_cast`. Without this, every table-name-qualified column
19704        // on a synthesised catalog answered "missing FROM-clause
19705        // entry" — which is the wall pg_dump hit on its first
19706        // pg_proc/pg_cast query.
19707        let alias = match (&alias, &meta_original) {
19708            (None, Some(orig)) if *orig != name => Some(orig.clone()),
19709            _ => alias,
19710        };
19711        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19712        // (PG grammar). BERNOULLI lowers to a per-row
19713        // `random() < p/100` conjunct on the enclosing SELECT's
19714        // WHERE — exact row-level Bernoulli semantics. SYSTEM
19715        // shares the lowering: SPG has no page structure to
19716        // sample, and the row-level form returns the same expected
19717        // fraction. REPEATABLE(seed) promises a deterministic
19718        // sample SPG cannot honour yet — honest error rather than
19719        // a silently ignored seed.
19720        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19721            self.advance();
19722            let method = self.expect_ident_like()?;
19723            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19724                return Err(self.err(alloc::format!(
19725                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19726                )));
19727            }
19728            if !matches!(self.peek(), Token::LParen) {
19729                return Err(self.err(alloc::format!(
19730                    "expected '(' after TABLESAMPLE {}, got {:?}",
19731                    method.to_ascii_uppercase(),
19732                    self.peek()
19733                )));
19734            }
19735            self.advance();
19736            let percent = self.parse_expr(0)?;
19737            if !matches!(self.peek(), Token::RParen) {
19738                return Err(self.err(alloc::format!(
19739                    "expected ')' after TABLESAMPLE percentage, got {:?}",
19740                    self.peek()
19741                )));
19742            }
19743            self.advance();
19744            // REPEATABLE(seed) → a deterministic per-row draw seeded by
19745            // `seed`, so the sample is stable across repeats and rescans.
19746            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19747            let mut sample_seed: Option<Expr> = None;
19748            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19749                self.advance();
19750                if !matches!(self.peek(), Token::LParen) {
19751                    return Err(self.err(alloc::format!(
19752                        "expected '(' after REPEATABLE, got {:?}",
19753                        self.peek()
19754                    )));
19755                }
19756                self.advance();
19757                let seed = self.parse_expr(0)?;
19758                if !matches!(self.peek(), Token::RParen) {
19759                    return Err(self.err(alloc::format!(
19760                        "expected ')' after REPEATABLE seed, got {:?}",
19761                        self.peek()
19762                    )));
19763                }
19764                self.advance();
19765                sample_seed = Some(seed);
19766            }
19767            let draw = match sample_seed {
19768                Some(seed) => Expr::FunctionCall {
19769                    name: "__tsm_fract".to_string(),
19770                    args: alloc::vec![seed],
19771                },
19772                None => Expr::FunctionCall {
19773                    name: "random".to_string(),
19774                    args: Vec::new(),
19775                },
19776            };
19777            self.pending_sample_preds.push(Expr::Binary {
19778                lhs: Box::new(draw),
19779                op: crate::ast::BinOp::Lt,
19780                rhs: Box::new(Expr::Binary {
19781                    lhs: Box::new(percent),
19782                    op: crate::ast::BinOp::Div,
19783                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19784                }),
19785            });
19786        }
19787        Ok(TableRef {
19788            name,
19789            alias,
19790            only,
19791            as_of_segment,
19792            unnest_expr: None,
19793            unnest_column_aliases: Vec::new(),
19794            with_ordinality: false,
19795            generate_series_args: None,
19796            lateral_subquery: None,
19797            jsonb_each_text_arg: None,
19798            table_fn_call: None,
19799            rows_from: None,
19800            json_table: None,
19801            scalar_fn_item: false,
19802        })
19803    }
19804
19805    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19806    /// but also accepts `AS alias(col [, col, …])` — the
19807    /// PG-standard table-function column-list form. The column
19808    /// list is only honoured when paired with `UNNEST(...)` in
19809    /// the parent; other call sites currently discard it.
19810    /// True when the expression tree contains a qualified column
19811    /// reference (`t.col`) — the syntactic marker that an SRF
19812    /// argument correlates with a preceding FROM item.
19813    fn expr_has_qualified_column(e: &Expr) -> bool {
19814        match e {
19815            Expr::Column(c) => c.qualifier.is_some(),
19816            Expr::Binary { lhs, rhs, .. } => {
19817                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19818            }
19819            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19820            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19821            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19822            Expr::Case {
19823                operand,
19824                branches,
19825                else_branch,
19826            } => {
19827                operand
19828                    .as_deref()
19829                    .is_some_and(Self::expr_has_qualified_column)
19830                    || branches.iter().any(|(w, t)| {
19831                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19832                    })
19833                    || else_branch
19834                        .as_deref()
19835                        .is_some_and(Self::expr_has_qualified_column)
19836            }
19837            _ => false,
19838        }
19839    }
19840
19841    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19842    /// counts a bare (unqualified) column. A set-returning function has no
19843    /// input columns of its own, so ANY column in its arguments is an outer
19844    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19845    fn expr_has_any_column(e: &Expr) -> bool {
19846        match e {
19847            Expr::Column(_) => true,
19848            Expr::Binary { lhs, rhs, .. } => {
19849                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19850            }
19851            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19852            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19853            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19854            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19855            // constructor or subscript fell to the `_ => false` arm, so
19856            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19857            // channel and the eager peer eval answered `column "x" does
19858            // not exist` (the substitution walker already recurses both
19859            // shapes; only this detector was blind to them).
19860            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19861            Expr::ArraySubscript { target, index } => {
19862                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19863            }
19864            Expr::Case {
19865                operand,
19866                branches,
19867                else_branch,
19868            } => {
19869                operand.as_deref().is_some_and(Self::expr_has_any_column)
19870                    || branches
19871                        .iter()
19872                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19873                    || else_branch
19874                        .as_deref()
19875                        .is_some_and(Self::expr_has_any_column)
19876            }
19877            _ => false,
19878        }
19879    }
19880
19881    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19882    /// `generate_series(1, t.n)`) into the lateral_subquery
19883    /// channel: `SELECT * FROM <srf>` executes per outer row with
19884    /// outer references substituted (v7.37.43-T4.5 machinery).
19885    /// Uncorrelated SRFs stay on their plain channels.
19886    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19887        let name = srf.name.clone();
19888        let alias = srf.alias.clone();
19889        let inner = crate::ast::SelectStatement {
19890            locking: None,
19891            ctes: Vec::new(),
19892            distinct: false,
19893            distinct_on: Vec::new(),
19894            items: alloc::vec![crate::ast::SelectItem::Wildcard],
19895            from: Some(crate::ast::FromClause {
19896                primary: srf,
19897                joins: Vec::new(),
19898            }),
19899            where_: None,
19900            group_by: None,
19901            group_by_all: false,
19902            having: None,
19903            unions: Vec::new(),
19904            order_by: Vec::new(),
19905            limit: None,
19906            offset: None,
19907            limit_with_ties: false,
19908            window_check_exprs: Vec::new(),
19909        };
19910        TableRef {
19911            name,
19912            alias,
19913            only: false,
19914            as_of_segment: None,
19915            unnest_expr: None,
19916            unnest_column_aliases: Vec::new(),
19917            with_ordinality: false,
19918            generate_series_args: None,
19919            lateral_subquery: Some(Box::new(inner)),
19920            jsonb_each_text_arg: None,
19921            table_fn_call: None,
19922            rows_from: None,
19923            json_table: None,
19924            scalar_fn_item: false,
19925        }
19926    }
19927
19928    /// True when the expression tree contains an unresolved
19929    /// `OVER w` marker (see parse_over_clause).
19930    fn expr_has_named_window(e: &Expr) -> bool {
19931        match e {
19932            Expr::WindowFunction { partition_by, .. } => matches!(
19933                partition_by.as_slice(),
19934                [Expr::Column(c)] if matches!(
19935                    c.qualifier.as_deref(),
19936                    Some("__named_window__") | Some("__named_window_ref__")
19937                )
19938            ),
19939            Expr::Binary { lhs, rhs, .. } => {
19940                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19941            }
19942            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19943            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19944            Expr::Case {
19945                operand,
19946                branches,
19947                else_branch,
19948            } => {
19949                operand.as_deref().is_some_and(Self::expr_has_named_window)
19950                    || branches.iter().any(|(w, t)| {
19951                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19952                    })
19953                    || else_branch
19954                        .as_deref()
19955                        .is_some_and(Self::expr_has_named_window)
19956            }
19957            _ => false,
19958        }
19959    }
19960
19961    /// v7.39 (round 705) — the NAMES the expression references through the
19962    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19963    /// definitions nothing referenced. Traversal mirrors
19964    /// `expr_has_named_window` above.
19965    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19966        match e {
19967            Expr::WindowFunction { partition_by, .. } => {
19968                if let [Expr::Column(c)] = partition_by.as_slice()
19969                    && matches!(
19970                        c.qualifier.as_deref(),
19971                        Some("__named_window__") | Some("__named_window_ref__")
19972                    )
19973                {
19974                    into.push(c.name.clone());
19975                }
19976            }
19977            Expr::Binary { lhs, rhs, .. } => {
19978                Self::collect_named_window_refs(lhs, into);
19979                Self::collect_named_window_refs(rhs, into);
19980            }
19981            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19982                Self::collect_named_window_refs(expr, into);
19983            }
19984            Expr::FunctionCall { args, .. } => {
19985                for a in args {
19986                    Self::collect_named_window_refs(a, into);
19987                }
19988            }
19989            Expr::Case {
19990                operand,
19991                branches,
19992                else_branch,
19993            } => {
19994                if let Some(o) = operand.as_deref() {
19995                    Self::collect_named_window_refs(o, into);
19996                }
19997                for (w, t) in branches {
19998                    Self::collect_named_window_refs(w, into);
19999                    Self::collect_named_window_refs(t, into);
20000                }
20001                if let Some(eb) = else_branch.as_deref() {
20002                    Self::collect_named_window_refs(eb, into);
20003                }
20004            }
20005            _ => {}
20006        }
20007    }
20008
20009    /// Inline named-window definitions into the `OVER w` markers.
20010    /// An unknown name errors (PG: window "w" does not exist).
20011    #[allow(clippy::type_complexity)]
20012    fn substitute_named_windows(
20013        e: &mut Expr,
20014        defs: &[(
20015            String,
20016            (
20017                Vec<Expr>,
20018                Vec<(Expr, bool, Option<bool>)>,
20019                Option<WindowFrame>,
20020            ),
20021        )],
20022    ) -> Result<(), String> {
20023        match e {
20024            Expr::WindowFunction {
20025                partition_by,
20026                order_by,
20027                frame,
20028                ..
20029            } => {
20030                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20031                // from the bare `OVER w1` (a plain reference).
20032                let named = match partition_by.as_slice() {
20033                    [Expr::Column(c)] => match c.qualifier.as_deref() {
20034                        Some("__named_window__") => Some((c.name.clone(), false)),
20035                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
20036                        _ => None,
20037                    },
20038                    _ => None,
20039                };
20040                if let Some((wname, is_copy)) = named {
20041                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20042                    else {
20043                        return Err(alloc::format!("window {wname:?} does not exist"));
20044                    };
20045                    if !is_copy {
20046                        *partition_by = def.0.clone();
20047                        *order_by = def.1.clone();
20048                        *frame = def.2.clone();
20049                        return Ok(());
20050                    }
20051                    // v7.39 (round 229) — PG's copy rules, probed against
20052                    // 18.4: a copy inherits the partitioning, may supply an
20053                    // ordering only when the base has none, and may not copy
20054                    // a base that already carries a frame (its own frame
20055                    // would be ambiguous with the inherited one).
20056                    if !def.1.is_empty() && !order_by.is_empty() {
20057                        return Err(alloc::format!(
20058                            "cannot override ORDER BY clause of window \"{wname}\""
20059                        ));
20060                    }
20061                    if def.2.is_some() {
20062                        return Err(alloc::format!(
20063                            "cannot copy window \"{wname}\" because it has a frame clause"
20064                        ));
20065                    }
20066                    *partition_by = def.0.clone();
20067                    if order_by.is_empty() {
20068                        *order_by = def.1.clone();
20069                    }
20070                }
20071                Ok(())
20072            }
20073            Expr::Binary { lhs, rhs, .. } => {
20074                Self::substitute_named_windows(lhs, defs)?;
20075                Self::substitute_named_windows(rhs, defs)
20076            }
20077            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20078                Self::substitute_named_windows(expr, defs)
20079            }
20080            Expr::FunctionCall { args, .. } => {
20081                for a in args {
20082                    Self::substitute_named_windows(a, defs)?;
20083                }
20084                Ok(())
20085            }
20086            Expr::Case {
20087                operand,
20088                branches,
20089                else_branch,
20090            } => {
20091                if let Some(op) = operand {
20092                    Self::substitute_named_windows(op, defs)?;
20093                }
20094                for (w, t) in branches {
20095                    Self::substitute_named_windows(w, defs)?;
20096                    Self::substitute_named_windows(t, defs)?;
20097                }
20098                if let Some(el) = else_branch {
20099                    Self::substitute_named_windows(el, defs)?;
20100                }
20101                Ok(())
20102            }
20103            _ => Ok(()),
20104        }
20105    }
20106
20107    /// SQL-standard `TABLE name` shorthand — builds the equivalent
20108    /// `SELECT * FROM name` head. Callers own set-op chain / tail
20109    /// composition.
20110    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20111        debug_assert!(matches!(self.peek(), Token::Table));
20112        self.advance(); // TABLE
20113        let tname = self.expect_ident_like()?;
20114        Ok(SelectStatement {
20115            locking: None,
20116            ctes: Vec::new(),
20117            distinct: false,
20118            distinct_on: Vec::new(),
20119            items: alloc::vec![SelectItem::Wildcard],
20120            from: Some(FromClause {
20121                primary: TableRef {
20122                    name: tname,
20123                    alias: None,
20124                    only: false,
20125                    as_of_segment: None,
20126                    unnest_expr: None,
20127                    unnest_column_aliases: Vec::new(),
20128                    with_ordinality: false,
20129                    generate_series_args: None,
20130                    lateral_subquery: None,
20131                    jsonb_each_text_arg: None,
20132                    table_fn_call: None,
20133                    rows_from: None,
20134                    json_table: None,
20135                    scalar_fn_item: false,
20136                },
20137                joins: Vec::new(),
20138            }),
20139            where_: None,
20140            group_by: None,
20141            group_by_all: false,
20142            having: None,
20143            unions: Vec::new(),
20144            order_by: Vec::new(),
20145            limit: None,
20146            offset: None,
20147            limit_with_ties: false,
20148            window_check_exprs: Vec::new(),
20149        })
20150    }
20151
20152    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20153    /// variants) → a derived table that reads each declared column out of
20154    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20155    /// `jsonb_array_elements(J)` (one row per element, column `value`);
20156    /// the scalar *record form projects a single row straight off `J`.
20157    /// Rides the existing lateral-subquery channel, so no new executor or
20158    /// AST is needed.
20159    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20160        use crate::ast::{
20161            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20162        };
20163        let fn_name = match self.peek() {
20164            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20165            _ => unreachable!("caller guarded is_json_to_record_name"),
20166        };
20167        self.advance(); // fn name
20168        self.advance(); // (
20169        let mut arg = self.parse_expr(0)?;
20170        // populate_record(base, json): the base only carries the record
20171        // type here — the JSON argument is the second expression.
20172        let mut base: Option<Expr> = None;
20173        if matches!(self.peek(), Token::Comma) {
20174            self.advance();
20175            base = Some(arg);
20176            arg = self.parse_expr(0)?;
20177        }
20178        if !matches!(self.peek(), Token::RParen) {
20179            return Err(self.err(alloc::format!(
20180                "expected ')' after {fn_name}() argument, got {:?}",
20181                self.peek()
20182            )));
20183        }
20184        self.advance(); // )
20185        let is_set = fn_name.ends_with("recordset");
20186        // `[AS] alias ( col type [, …] )` column-definition list.
20187        if matches!(self.peek(), Token::As) {
20188            self.advance();
20189        }
20190        let alias_opt = match self.peek() {
20191            Token::Ident(s) | Token::QuotedIdent(s) => {
20192                let a = s.clone();
20193                self.advance();
20194                Some(a)
20195            }
20196            _ => None,
20197        };
20198        // v7.39 (read01 round 76) — the populate family's canonical PG
20199        // spelling carries no column list at all: the row shape comes from
20200        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20201        // j)`). The parser has no catalog, so hand the two arguments to the
20202        // engine's table-function channel, which does. Only `*_to_record*`
20203        // (whose base is bare `record`) genuinely requires the list.
20204        if !matches!(self.peek(), Token::LParen) {
20205            if let Some(base_expr) = base {
20206                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20207                return Ok(TableRef {
20208                    name: alias.clone(),
20209                    alias: Some(alias),
20210                    only: false,
20211                    as_of_segment: None,
20212                    unnest_expr: None,
20213                    unnest_column_aliases: Vec::new(),
20214                    with_ordinality: false,
20215                    generate_series_args: None,
20216                    lateral_subquery: None,
20217                    jsonb_each_text_arg: None,
20218                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20219                    rows_from: None,
20220                    json_table: None,
20221                    scalar_fn_item: false,
20222                });
20223            }
20224            return Err(self.err(alloc::format!(
20225                "expected '(' to start the {fn_name} column-definition list, got {:?}",
20226                self.peek()
20227            )));
20228        }
20229        let Some(alias) = alias_opt else {
20230            return Err(self.err(alloc::format!(
20231                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20232            )));
20233        };
20234        self.advance(); // (
20235        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20236        loop {
20237            let col = self.expect_ident_like()?;
20238            let ty = self.parse_cast_target()?;
20239            coldefs.push((col, ty));
20240            if matches!(self.peek(), Token::Comma) {
20241                self.advance();
20242                continue;
20243            }
20244            if matches!(self.peek(), Token::RParen) {
20245                self.advance();
20246                break;
20247            }
20248            return Err(self.err(alloc::format!(
20249                "expected ',' or ')' in {fn_name} column list, got {:?}",
20250                self.peek()
20251            )));
20252        }
20253        if coldefs.is_empty() {
20254            return Err(self.err(alloc::format!(
20255                "{fn_name} column-definition list must declare at least one column"
20256            )));
20257        }
20258        // Per column: (base ->> 'col')::type AS col. The base is the
20259        // per-element `value` column for the *set form, or the argument
20260        // itself for the scalar record form.
20261        let items: Vec<SelectItem> = coldefs
20262            .into_iter()
20263            .map(|(col, ty)| {
20264                let base = if is_set {
20265                    Expr::Column(ColumnName {
20266                        qualifier: None,
20267                        name: "value".to_string(),
20268                    })
20269                } else {
20270                    arg.clone()
20271                };
20272                SelectItem::Expr {
20273                    expr: Expr::Cast {
20274                        expr: Box::new(Expr::Binary {
20275                            lhs: Box::new(base),
20276                            op: BinOp::JsonGetText,
20277                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20278                        }),
20279                        target: ty,
20280                    },
20281                    alias: Some(col),
20282                }
20283            })
20284            .collect();
20285        let from = if is_set {
20286            let elem_fn = if fn_name.starts_with("jsonb") {
20287                "jsonb_array_elements"
20288            } else {
20289                "json_array_elements"
20290            };
20291            Some(FromClause {
20292                primary: TableRef {
20293                    name: "value".to_string(),
20294                    alias: None,
20295                    only: false,
20296                    as_of_segment: None,
20297                    unnest_expr: Some(Box::new(Expr::FunctionCall {
20298                        name: elem_fn.to_string(),
20299                        args: alloc::vec![arg],
20300                    })),
20301                    unnest_column_aliases: alloc::vec!["value".to_string()],
20302                    with_ordinality: false,
20303                    generate_series_args: None,
20304                    lateral_subquery: None,
20305                    jsonb_each_text_arg: None,
20306                    table_fn_call: None,
20307                    rows_from: None,
20308                    json_table: None,
20309                    scalar_fn_item: false,
20310                },
20311                joins: Vec::new(),
20312            })
20313        } else {
20314            None
20315        };
20316        let inner = SelectStatement {
20317            locking: None,
20318            ctes: Vec::new(),
20319            distinct: false,
20320            distinct_on: Vec::new(),
20321            items,
20322            from,
20323            where_: None,
20324            group_by: None,
20325            group_by_all: false,
20326            having: None,
20327            unions: Vec::new(),
20328            order_by: Vec::new(),
20329            limit: None,
20330            offset: None,
20331            limit_with_ties: false,
20332            window_check_exprs: Vec::new(),
20333        };
20334        Ok(TableRef {
20335            name: alias.clone(),
20336            alias: Some(alias),
20337            only: false,
20338            as_of_segment: None,
20339            unnest_expr: None,
20340            unnest_column_aliases: Vec::new(),
20341            with_ordinality: false,
20342            generate_series_args: None,
20343            lateral_subquery: Some(Box::new(inner)),
20344            jsonb_each_text_arg: None,
20345            table_fn_call: None,
20346            rows_from: None,
20347            json_table: None,
20348            scalar_fn_item: false,
20349        })
20350    }
20351
20352    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20353    /// Returns true when the clause was present. `WITH` alone (a
20354    /// CTE can never start here) is not enough — the ORDINALITY
20355    /// ident must follow, so a stray WITH still errors downstream.
20356    fn absorb_with_ordinality(&mut self) -> bool {
20357        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20358            && matches!(self.tokens.get(self.pos + 1),
20359                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20360        {
20361            self.advance();
20362            self.advance();
20363            true
20364        } else {
20365            false
20366        }
20367    }
20368
20369    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20370    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20371    /// Out-of-line: the caller sits on the FROM recursion chain.
20372    #[inline(never)]
20373    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20374        let fn_name = match self.advance() {
20375            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20376            _ => unreachable!("caller peeked an ident"),
20377        };
20378        self.advance(); // (
20379        let mut args: Vec<Expr> = Vec::new();
20380        if !matches!(self.peek(), Token::RParen) {
20381            loop {
20382                args.push(self.parse_expr(0)?);
20383                if matches!(self.peek(), Token::Comma) {
20384                    self.advance();
20385                    continue;
20386                }
20387                break;
20388            }
20389        }
20390        if !matches!(self.peek(), Token::RParen) {
20391            return Err(self.err(alloc::format!(
20392                "expected ')' after {fn_name}() arguments, got {:?}",
20393                self.peek()
20394            )));
20395        }
20396        self.advance();
20397        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20398        // counter column rides after the function's own, and the alias list
20399        // names it.
20400        let with_ordinality = self.absorb_with_ordinality();
20401        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20402        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20403        Ok(TableRef {
20404            name,
20405            alias: alias_ident,
20406            only: false,
20407            as_of_segment: None,
20408            unnest_expr: None,
20409            unnest_column_aliases,
20410            with_ordinality,
20411            generate_series_args: None,
20412            lateral_subquery: None,
20413            jsonb_each_text_arg: None,
20414            table_fn_call: Some(Box::new((fn_name, args))),
20415            rows_from: None,
20416            json_table: None,
20417            scalar_fn_item: false,
20418        })
20419    }
20420
20421    /// v7.39 (round 205, JSON_TABLE) — parse
20422    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20423    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20424    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20425    #[inline(never)]
20426    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20427        self.advance(); // json_table
20428        self.advance(); // (
20429        let doc = Box::new(self.parse_expr(0)?);
20430        self.expect_comma_json_table()?;
20431        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20432        // Optional `PASSING <expr> AS <name> [, …]`.
20433        let mut passing: Vec<(String, Expr)> = Vec::new();
20434        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20435            self.advance();
20436            loop {
20437                let e = self.parse_expr(0)?;
20438                if !matches!(self.peek(), Token::As) {
20439                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20440                }
20441                self.advance();
20442                let vname = match self.advance() {
20443                    Token::Ident(s) | Token::QuotedIdent(s) => s,
20444                    other => {
20445                        return Err(self.err(alloc::format!(
20446                            "expected PASSING variable name, got {other:?}"
20447                        )));
20448                    }
20449                };
20450                passing.push((vname, e));
20451                if matches!(self.peek(), Token::Comma) {
20452                    self.advance();
20453                    continue;
20454                }
20455                break;
20456            }
20457        }
20458        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20459            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20460        }
20461        self.advance();
20462        let columns = self.parse_json_table_columns()?;
20463        if !matches!(self.peek(), Token::RParen) {
20464            return Err(self.err(alloc::format!(
20465                "expected ')' to close JSON_TABLE, got {:?}",
20466                self.peek()
20467            )));
20468        }
20469        self.advance();
20470        let alias_ident = self.parse_optional_alias()?;
20471        let name = alias_ident
20472            .clone()
20473            .unwrap_or_else(|| String::from("json_table"));
20474        Ok(TableRef {
20475            name,
20476            alias: alias_ident,
20477            only: false,
20478            as_of_segment: None,
20479            unnest_expr: None,
20480            unnest_column_aliases: Vec::new(),
20481            with_ordinality: false,
20482            generate_series_args: None,
20483            lateral_subquery: None,
20484            jsonb_each_text_arg: None,
20485            table_fn_call: None,
20486            rows_from: None,
20487            json_table: Some(Box::new(crate::ast::JsonTable {
20488                doc,
20489                row_path,
20490                columns,
20491                passing,
20492            })),
20493            scalar_fn_item: false,
20494        })
20495    }
20496
20497    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20498        if !matches!(self.peek(), Token::Comma) {
20499            return Err(self.err(alloc::format!(
20500                "expected ',' after JSON_TABLE document, got {:?}",
20501                self.peek()
20502            )));
20503        }
20504        self.advance();
20505        Ok(())
20506    }
20507
20508    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20509        match self.advance() {
20510            Token::String(s) => Ok(s),
20511            other => Err(self.err(alloc::format!(
20512                "expected {what} string literal, got {other:?}"
20513            ))),
20514        }
20515    }
20516
20517    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20518    #[inline(never)]
20519    fn parse_json_table_columns(
20520        &mut self,
20521    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20522        if !matches!(self.peek(), Token::LParen) {
20523            return Err(self.err("expected '(' after COLUMNS".into()));
20524        }
20525        self.advance();
20526        let mut cols = Vec::new();
20527        loop {
20528            cols.push(self.parse_json_table_one_column()?);
20529            if matches!(self.peek(), Token::Comma) {
20530                self.advance();
20531                continue;
20532            }
20533            break;
20534        }
20535        if !matches!(self.peek(), Token::RParen) {
20536            return Err(self.err(alloc::format!(
20537                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20538                self.peek()
20539            )));
20540        }
20541        self.advance();
20542        Ok(cols)
20543    }
20544
20545    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20546        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20547        // NESTED [PATH] '<p>' COLUMNS (...)
20548        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20549            self.advance();
20550            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20551                self.advance();
20552            }
20553            let path = self.parse_json_string_literal("NESTED PATH")?;
20554            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20555                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20556            }
20557            self.advance();
20558            let columns = self.parse_json_table_columns()?;
20559            return Ok(JsonTableColumn::Nested { path, columns });
20560        }
20561        // <name> ...
20562        let name = match self.advance() {
20563            Token::Ident(s) | Token::QuotedIdent(s) => s,
20564            other => {
20565                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20566            }
20567        };
20568        // <name> FOR ORDINALITY
20569        if matches!(self.peek(), Token::For) {
20570            self.advance();
20571            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20572                return Err(self.err("expected ORDINALITY after FOR".into()));
20573            }
20574            self.advance();
20575            return Ok(JsonTableColumn::Ordinality { name });
20576        }
20577        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20578        let ty = self.parse_column_type_name()?;
20579        let mut format_json = false;
20580        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20581            self.advance();
20582            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20583                return Err(self.err("expected JSON after FORMAT".into()));
20584            }
20585            self.advance();
20586            format_json = true;
20587        }
20588        let mut exists = false;
20589        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20590            self.advance();
20591            exists = true;
20592        }
20593        let mut path = alloc::format!("$.{name}");
20594        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20595            self.advance();
20596            path = self.parse_json_string_literal("column PATH")?;
20597        }
20598        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20599            // `FORMAT JSON` after PATH (alternate placement).
20600            self.advance();
20601            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20602                self.advance();
20603            }
20604            format_json = true;
20605        }
20606        let mut wrapper = false;
20607        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20608            self.advance();
20609            // optional CONDITIONAL/UNCONDITIONAL
20610            if matches!(self.peek(), Token::Ident(s)
20611                if s.eq_ignore_ascii_case("unconditional")
20612                    || s.eq_ignore_ascii_case("conditional"))
20613            {
20614                self.advance();
20615            }
20616            if !matches!(self.peek(), Token::Ident(s)
20617                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20618            {
20619                return Err(self.err("expected WRAPPER after WITH".into()));
20620            }
20621            self.advance();
20622            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20623            if matches!(self.peek(), Token::Ident(s)
20624                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20625            {
20626                self.advance();
20627            }
20628            wrapper = true;
20629        }
20630        // ON EMPTY / ON ERROR clauses (two, in any order).
20631        let mut on_empty = JsonTableOnBehavior::Null;
20632        let mut on_error = JsonTableOnBehavior::Null;
20633        for _ in 0..2 {
20634            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20635            {
20636                self.advance();
20637                Some(JsonTableOnBehavior::Error)
20638            } else if matches!(self.peek(), Token::Null) {
20639                self.advance();
20640                Some(JsonTableOnBehavior::Null)
20641            } else if matches!(self.peek(), Token::Default) {
20642                self.advance();
20643                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20644            } else {
20645                None
20646            };
20647            let Some(behavior) = behavior else { break };
20648            // `ON {EMPTY|ERROR}`
20649            if !matches!(self.peek(), Token::On) {
20650                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20651            }
20652            self.advance();
20653            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20654                self.advance();
20655                on_empty = behavior;
20656            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20657                self.advance();
20658                on_error = behavior;
20659            } else {
20660                return Err(self.err("expected EMPTY or ERROR after ON".into()));
20661            }
20662        }
20663        Ok(JsonTableColumn::Regular {
20664            name,
20665            ty,
20666            path,
20667            exists,
20668            format_json,
20669            wrapper,
20670            on_empty,
20671            on_error,
20672        })
20673    }
20674
20675    fn parse_optional_alias_with_columns(
20676        &mut self,
20677    ) -> Result<(Option<String>, Vec<String>), ParseError> {
20678        let alias = self.parse_optional_alias()?;
20679        if alias.is_none() {
20680            return Ok((None, Vec::new()));
20681        }
20682        let mut cols: Vec<String> = Vec::new();
20683        if matches!(self.peek(), Token::LParen) {
20684            self.advance();
20685            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20686                self.advance();
20687                cols.push(s);
20688                if matches!(self.peek(), Token::Comma) {
20689                    self.advance();
20690                    continue;
20691                }
20692                break;
20693            }
20694            if matches!(self.peek(), Token::RParen) {
20695                self.advance();
20696            }
20697        }
20698        Ok((alias, cols))
20699    }
20700
20701    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20702    /// whose keyword token was already consumed and whose `(` is the
20703    /// current token. Factored out of `parse_atom` (and marked
20704    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20705    /// recursive `parse_atom` frame — inlining them there enlarges the
20706    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20707    /// against, risking an overflow before the budget triggers.
20708    #[inline(never)]
20709    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20710        self.advance(); // (
20711        let mut args = Vec::new();
20712        if !matches!(self.peek(), Token::RParen) {
20713            loop {
20714                args.push(self.parse_expr(0)?);
20715                match self.peek() {
20716                    Token::Comma => {
20717                        self.advance();
20718                    }
20719                    Token::RParen => break,
20720                    other => {
20721                        return Err(self.err(alloc::format!(
20722                            "expected ',' or ')' in {name}() args, got {other:?}"
20723                        )));
20724                    }
20725                }
20726            }
20727        }
20728        self.advance(); // )
20729        Ok(Expr::FunctionCall {
20730            name: name.into(),
20731            args,
20732        })
20733    }
20734
20735    /// FROM-clause: a primary table reference plus zero-or-more joined
20736    /// peers expressed via either `, <table>` (cross-product, no ON) or
20737    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20738    /// v1.10 keeps the join list flat (left-associative nested-loop
20739    /// semantics).
20740    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20741        let primary = self.parse_table_ref()?;
20742        let primary_qual = primary
20743            .alias
20744            .clone()
20745            .unwrap_or_else(|| primary.name.clone());
20746        let joins = self.parse_from_joins(&primary_qual)?;
20747        Ok(FromClause { primary, joins })
20748    }
20749
20750    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20751    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20752    /// SAME grammar after its target table has already been consumed.
20753    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20754    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20755    /// be parsed forward, once.)
20756    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20757    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20758    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20759    /// desugaring, which needs a name for the left side of each equality.
20760    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20761        let mut joins = Vec::new();
20762        loop {
20763            // `, <table>` — cross-product with no ON.
20764            if matches!(self.peek(), Token::Comma) {
20765                self.advance();
20766                let table = self.parse_table_ref()?;
20767                joins.push(FromJoin {
20768                    kind: JoinKind::Cross,
20769                    table,
20770                    on: None,
20771                    using_cols: None,
20772                    natural: false,
20773                });
20774                continue;
20775            }
20776            // v7.37.16 — optional leading `NATURAL` before the join
20777            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20778            // not a lexer keyword (it arrives as a bare Ident), so match
20779            // it case-insensitively here. When present, no ON/USING
20780            // clause is allowed — the common columns are resolved at
20781            // execution time.
20782            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20783            if natural {
20784                self.advance();
20785            }
20786            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20787            // CROSS JOIN, and bare JOIN (defaults to INNER).
20788            let kind =
20789                match self.peek() {
20790                    Token::Inner => {
20791                        self.advance();
20792                        if !matches!(self.peek(), Token::Join) {
20793                            return Err(self
20794                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20795                        }
20796                        self.advance();
20797                        JoinKind::Inner
20798                    }
20799                    Token::Left => {
20800                        self.advance();
20801                        if matches!(self.peek(), Token::Outer) {
20802                            self.advance();
20803                        }
20804                        if !matches!(self.peek(), Token::Join) {
20805                            return Err(self.err(format!(
20806                                "expected JOIN after LEFT [OUTER], got {:?}",
20807                                self.peek()
20808                            )));
20809                        }
20810                        self.advance();
20811                        JoinKind::Left
20812                    }
20813                    Token::Cross => {
20814                        self.advance();
20815                        if !matches!(self.peek(), Token::Join) {
20816                            return Err(self
20817                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20818                        }
20819                        self.advance();
20820                        JoinKind::Cross
20821                    }
20822                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20823                    Token::Right => {
20824                        self.advance();
20825                        if matches!(self.peek(), Token::Outer) {
20826                            self.advance();
20827                        }
20828                        if !matches!(self.peek(), Token::Join) {
20829                            return Err(self.err(format!(
20830                                "expected JOIN after RIGHT [OUTER], got {:?}",
20831                                self.peek()
20832                            )));
20833                        }
20834                        self.advance();
20835                        JoinKind::Right
20836                    }
20837                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20838                    Token::Full => {
20839                        self.advance();
20840                        if matches!(self.peek(), Token::Outer) {
20841                            self.advance();
20842                        }
20843                        if !matches!(self.peek(), Token::Join) {
20844                            return Err(self.err(format!(
20845                                "expected JOIN after FULL [OUTER], got {:?}",
20846                                self.peek()
20847                            )));
20848                        }
20849                        self.advance();
20850                        JoinKind::FullOuter
20851                    }
20852                    Token::Join => {
20853                        self.advance();
20854                        JoinKind::Inner
20855                    }
20856                    _ => break,
20857                };
20858            let table = self.parse_table_ref()?;
20859            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20860            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20861            // where prev_table is the most-recent left-side table
20862            // (the previous join's table if any, else the FROM primary).
20863            // PG semantics around column merging are richer (USING'd
20864            // cols become deduplicated single output columns); for
20865            // sugar purposes the predicate-only form covers the
20866            // baseline corpus shape and chained `… JOIN x USING (k)
20867            // JOIN y USING (k)` calls.
20868            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20869            // common columns resolve at execution time.
20870            if natural {
20871                joins.push(FromJoin {
20872                    kind,
20873                    table,
20874                    on: None,
20875                    using_cols: None,
20876                    natural: true,
20877                });
20878                continue;
20879            }
20880            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20881            // v7.37.16 — capture the USING column list (in addition to
20882            // the ON desugar below) so the executor can perform PG's
20883            // column-merge on the output side.
20884            let mut using_cols: Option<Vec<String>> = None;
20885            let on = if matches!(self.peek(), Token::On) {
20886                self.advance();
20887                Some(self.parse_expr(0)?)
20888            } else if using_match {
20889                self.advance();
20890                if !matches!(self.peek(), Token::LParen) {
20891                    return Err(
20892                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20893                    );
20894                }
20895                self.advance();
20896                let mut cols: Vec<String> = Vec::new();
20897                loop {
20898                    match self.peek().clone() {
20899                        Token::Ident(s) | Token::QuotedIdent(s) => {
20900                            self.advance();
20901                            cols.push(s);
20902                        }
20903                        other => {
20904                            return Err(self.err(format!(
20905                                "expected column name inside USING (…), got {other:?}"
20906                            )));
20907                        }
20908                    }
20909                    match self.peek() {
20910                        Token::Comma => {
20911                            self.advance();
20912                            continue;
20913                        }
20914                        Token::RParen => {
20915                            self.advance();
20916                            break;
20917                        }
20918                        other => {
20919                            return Err(self.err(format!(
20920                                "expected ',' or ')' inside USING (…), got {other:?}"
20921                            )));
20922                        }
20923                    }
20924                }
20925                if cols.is_empty() {
20926                    return Err(self.err("USING (…) requires at least one column".to_string()));
20927                }
20928                using_cols = Some(cols.clone());
20929                // Pick the left-side alias: prev join's table if any,
20930                // else FROM primary. Use alias when present, else
20931                // table name (PG-equivalent qualifier).
20932                let left_qual: String = joins
20933                    .last()
20934                    .map(|j| {
20935                        j.table
20936                            .alias
20937                            .clone()
20938                            .unwrap_or_else(|| j.table.name.clone())
20939                    })
20940                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20941                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20942                let mut iter = cols.into_iter().map(|c| Expr::Binary {
20943                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20944                        qualifier: Some(left_qual.clone()),
20945                        name: c.clone(),
20946                    })),
20947                    op: crate::ast::BinOp::Eq,
20948                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20949                        qualifier: Some(right_qual.clone()),
20950                        name: c,
20951                    })),
20952                });
20953                let first = iter.next().expect("at least one col");
20954                Some(iter.fold(first, |acc, pred| Expr::Binary {
20955                    lhs: alloc::boxed::Box::new(acc),
20956                    op: crate::ast::BinOp::And,
20957                    rhs: alloc::boxed::Box::new(pred),
20958                }))
20959            } else if kind == JoinKind::Cross {
20960                None
20961            } else {
20962                return Err(self.err(format!(
20963                    "expected ON or USING after {:?} JOIN, got {:?}",
20964                    kind,
20965                    self.peek()
20966                )));
20967            };
20968            joins.push(FromJoin {
20969                kind,
20970                table,
20971                on,
20972                using_cols,
20973                natural: false,
20974            });
20975        }
20976        Ok(joins)
20977    }
20978
20979    /// Optional alias after an expression or table:
20980    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20981    /// accepted (PG-style implicit alias). Returns `None` if the next token
20982    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20983    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20984        if matches!(self.peek(), Token::As) {
20985            self.advance();
20986            // v7.39 (round 340, V56) — after AS the next token MUST be an
20987            // identifier. This used to return None and "let the caller
20988            // surface the error on the next expectation", but when AS is
20989            // the LAST token there is no next expectation: `SELECT 1 AS`
20990            // parsed clean and silently dropped the alias. PG rejects it.
20991            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20992                return self.expect_ident_like().map(Some);
20993            }
20994            return Err(self.err(alloc::format!(
20995                "expected an alias after AS, got {:?}",
20996                self.peek()
20997            )));
20998        }
20999        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21000        // grammar reserves a long list of follow-keywords from the
21001        // alias slot. SPG's bareword approximation: skip a small
21002        // set of idents that would otherwise be swallowed as the
21003        // table alias and break trailing clauses like CREATE
21004        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21005        // CONFLICT WHERE shapes.
21006        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21007            if is_alias_stopword(s) {
21008                return Ok(None);
21009            }
21010            return Ok(self.expect_ident_like().ok());
21011        }
21012        Ok(None)
21013    }
21014
21015    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21016    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21017        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21018        // error beats a stack overflow (an overflow aborts the
21019        // embedding host process).
21020        self.enter_nested()?;
21021        let r = self.parse_expr_inner(min_prec);
21022        self.nest_depth -= 1;
21023        r
21024    }
21025
21026    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21027    /// When the upcoming tokens form one, return the underlying
21028    /// operator token and the position just past the closing paren
21029    /// so the binary loop can dispatch on the plain operator.
21030    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21031        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21032            return None;
21033        }
21034        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21035            return None;
21036        }
21037        let mut i = self.pos + 2;
21038        // Optional schema qualifier (pg_catalog.<op> etc.).
21039        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21040            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21041        {
21042            i += 2;
21043        }
21044        let op_tok = self.tokens.get(i)?.clone();
21045        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21046            return None;
21047        }
21048        Some((i + 2, op_tok))
21049    }
21050
21051    /// PG operator symbols that lower onto function calls in
21052    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21053    /// family → regexp_like, comparison rung), `^@` (starts_with,
21054    /// comparison rung), `^` (power, tighter than `*`), `#`
21055    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21056    /// subset of the OR bits so the subtraction never borrows).
21057    fn try_symbol_operator(
21058        &mut self,
21059        lhs: &Expr,
21060        min_prec: u8,
21061    ) -> Result<Option<Expr>, ParseError> {
21062        enum Sym {
21063            Regex { ci: bool, negated: bool },
21064            Like { ci: bool, negated: bool },
21065            StartsWith,
21066            Power,
21067            Xor,
21068            RangeAdjacent,
21069        }
21070        // v7.39 (IS-precedence knife) — the low-precedence postfix
21071        // predicates ride this existing leaf call (zero new frame slots
21072        // on the nesting chain).
21073        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21074            return Ok(Some(e));
21075        }
21076        let (sym, prec): (Sym, u8) = match self.peek() {
21077            Token::Tilde => (
21078                Sym::Regex {
21079                    ci: false,
21080                    negated: false,
21081                },
21082                5,
21083            ),
21084            Token::TildeStar => (
21085                Sym::Regex {
21086                    ci: true,
21087                    negated: false,
21088                },
21089                5,
21090            ),
21091            Token::NotTilde => (
21092                Sym::Regex {
21093                    ci: false,
21094                    negated: true,
21095                },
21096                5,
21097            ),
21098            Token::NotTildeStar => (
21099                Sym::Regex {
21100                    ci: true,
21101                    negated: true,
21102                },
21103                5,
21104            ),
21105            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21106            Token::DoubleTilde => (
21107                Sym::Like {
21108                    ci: false,
21109                    negated: false,
21110                },
21111                5,
21112            ),
21113            Token::DoubleTildeStar => (
21114                Sym::Like {
21115                    ci: true,
21116                    negated: false,
21117                },
21118                5,
21119            ),
21120            Token::NotDoubleTilde => (
21121                Sym::Like {
21122                    ci: false,
21123                    negated: true,
21124                },
21125                5,
21126            ),
21127            Token::NotDoubleTildeStar => (
21128                Sym::Like {
21129                    ci: true,
21130                    negated: true,
21131                },
21132                5,
21133            ),
21134            Token::CaretAt => (Sym::StartsWith, 5),
21135            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21136            // tighter than `* / & |`, which the prec-9 rung preserves —
21137            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21138            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21139            Token::Caret => (Sym::Power, 9),
21140            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21141            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21142            Token::Hash => (Sym::Xor, 6),
21143            Token::Adjacent => (Sym::RangeAdjacent, 5),
21144            _ => return Ok(None),
21145        };
21146        if prec < min_prec {
21147            return Ok(None);
21148        }
21149        self.advance();
21150        let rhs = self.parse_expr(prec + 1)?;
21151        let out = match sym {
21152            Sym::Regex { ci, negated } => {
21153                let mut args = alloc::vec![lhs.clone(), rhs];
21154                if ci {
21155                    args.push(Expr::Literal(Literal::String(String::from("i"))));
21156                }
21157                maybe_not(
21158                    Expr::FunctionCall {
21159                        name: String::from("regexp_like"),
21160                        args,
21161                    },
21162                    negated,
21163                )
21164            }
21165            Sym::Like { ci, negated } => Expr::Like {
21166                expr: alloc::boxed::Box::new(lhs.clone()),
21167                pattern: alloc::boxed::Box::new(rhs),
21168                negated,
21169                case_insensitive: ci,
21170            },
21171            Sym::StartsWith => Expr::FunctionCall {
21172                name: String::from("starts_with"),
21173                args: alloc::vec![lhs.clone(), rhs],
21174            },
21175            Sym::Power => Expr::FunctionCall {
21176                name: String::from("power"),
21177                args: alloc::vec![lhs.clone(), rhs],
21178            },
21179            // `#` bitwise XOR — a real operator now (was desugared to
21180            // `(a|b)-(a&b)`, algebraically identical for integers but
21181            // undefined for bit strings; the direct op handles both).
21182            Sym::Xor => Expr::Binary {
21183                lhs: Box::new(lhs.clone()),
21184                op: BinOp::BitXor,
21185                rhs: Box::new(rhs),
21186            },
21187            // range `-|-` "is adjacent to" — lowered to a catalog function.
21188            Sym::RangeAdjacent => Expr::FunctionCall {
21189                name: String::from("range_adjacent"),
21190                args: alloc::vec![lhs.clone(), rhs],
21191            },
21192        };
21193        Ok(Some(out))
21194    }
21195
21196    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
21197    /// predicates, moved out of the tight postfix-cast loop: PG binds
21198    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
21199    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
21200    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
21201    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21202    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21203    /// when nothing at this position belongs to the family. Out-of-line
21204    /// (`inline(never)`): the caller sits on the per-nesting-level frame
21205    /// chain that MAX_NEST_DEPTH is tuned against.
21206    #[inline(never)]
21207    fn parse_postfix_predicate(
21208        &mut self,
21209        lhs: &Expr,
21210        min_prec: u8,
21211    ) -> Result<Option<Expr>, ParseError> {
21212        // Reached through try_symbol_operator (an existing leaf call of
21213        // the binary loop) so NO new stack slots land on the per-nesting
21214        // frame chain; the lhs clones only when a predicate actually
21215        // consumes it.
21216        match self.peek() {
21217            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21218            // comparison family rung 5 (each +1 from the pre-XOR ladder).
21219            Token::Is if min_prec <= 4 => {}
21220            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21221            Token::Not
21222                if min_prec <= 5
21223                    && matches!(
21224                        self.tokens.get(self.pos + 1),
21225                        Some(Token::Between | Token::In | Token::Like)
21226                    ) => {}
21227            Token::Not | Token::Ident(_)
21228                if min_prec <= 5
21229                    && (matches!(self.peek(), Token::Ident(s)
21230                            if s.eq_ignore_ascii_case("ilike")
21231                                || (self.mysql_dialect
21232                                    && (s.eq_ignore_ascii_case("regexp")
21233                                        || s.eq_ignore_ascii_case("rlike")))
21234                                || (s.eq_ignore_ascii_case("similar")
21235                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21236                        || (matches!(self.peek(), Token::Not)
21237                            && matches!(self.tokens.get(self.pos + 1),
21238                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21239                                    || (self.mysql_dialect
21240                                        && (s.eq_ignore_ascii_case("regexp")
21241                                            || s.eq_ignore_ascii_case("rlike")))
21242                                    || s.eq_ignore_ascii_case("similar")))) => {}
21243            _ => return Ok(None),
21244        }
21245        let mut expr = lhs.clone();
21246        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21247        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21248        if min_prec <= 4 {
21249            if matches!(self.peek(), Token::Is) {
21250                self.advance();
21251                let negated = if matches!(self.peek(), Token::Not) {
21252                    self.advance();
21253                    true
21254                } else {
21255                    false
21256                };
21257                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21258                // mailrs pg_dump.
21259                if matches!(self.peek(), Token::Distinct) {
21260                    self.advance();
21261                    if !matches!(self.peek(), Token::From) {
21262                        return Err(self.err(format!(
21263                            "expected FROM after IS{} DISTINCT, got {:?}",
21264                            if negated { " NOT" } else { "" },
21265                            self.peek()
21266                        )));
21267                    }
21268                    self.advance();
21269                    // Right-hand side: parse at the same precedence
21270                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21271                    // groups as `x IS DISTINCT FROM (a + b)`.
21272                    let rhs = self.parse_expr(5)?;
21273                    let op = if negated {
21274                        BinOp::IsNotDistinctFrom
21275                    } else {
21276                        BinOp::IsDistinctFrom
21277                    };
21278                    expr = Expr::Binary {
21279                        op,
21280                        lhs: Box::new(expr),
21281                        rhs: Box::new(rhs),
21282                    };
21283                    {
21284                        return Ok(Some(expr));
21285                    }
21286                }
21287                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21288                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21289                // Lowers onto pg_is_json(x, kind); NOT wraps the
21290                // call in a logical negation.
21291                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21292                if s.eq_ignore_ascii_case("json"))
21293                {
21294                    self.advance(); // JSON
21295                    let kind = match self.peek() {
21296                        Token::Ident(s) | Token::QuotedIdent(s)
21297                            if matches!(
21298                                s.to_ascii_lowercase().as_str(),
21299                                "value" | "object" | "array" | "scalar"
21300                            ) =>
21301                        {
21302                            let k = s.to_ascii_lowercase();
21303                            self.advance();
21304                            k
21305                        }
21306                        _ => "value".to_string(),
21307                    };
21308                    let call = Expr::FunctionCall {
21309                        name: "pg_is_json".to_string(),
21310                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21311                    };
21312                    expr = if negated {
21313                        Expr::Unary {
21314                            op: UnOp::Not,
21315                            expr: Box::new(call),
21316                        }
21317                    } else {
21318                        call
21319                    };
21320                    {
21321                        return Ok(Some(expr));
21322                    }
21323                }
21324                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21325                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21326                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21327                {
21328                    let form_kw = match self.peek() {
21329                        Token::Ident(s) | Token::QuotedIdent(s)
21330                            if matches!(
21331                                s.to_ascii_uppercase().as_str(),
21332                                "NFC" | "NFD" | "NFKC" | "NFKD"
21333                            ) && matches!(
21334                                self.tokens.get(self.pos + 1),
21335                                Some(Token::Ident(n) | Token::QuotedIdent(n))
21336                                    if n.eq_ignore_ascii_case("normalized")
21337                            ) =>
21338                        {
21339                            Some(s.to_ascii_uppercase())
21340                        }
21341                        _ => None,
21342                    };
21343                    let bare_normalized = form_kw.is_none()
21344                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21345                        if s.eq_ignore_ascii_case("normalized"));
21346                    if form_kw.is_some() || bare_normalized {
21347                        if form_kw.is_some() {
21348                            self.advance(); // form keyword
21349                        }
21350                        self.advance(); // NORMALIZED
21351                        let mut args = alloc::vec![expr];
21352                        if let Some(f) = form_kw {
21353                            args.push(Expr::Literal(Literal::String(f)));
21354                        }
21355                        let call = Expr::FunctionCall {
21356                            name: "is_normalized".to_string(),
21357                            args,
21358                        };
21359                        expr = if negated {
21360                            Expr::Unary {
21361                                op: UnOp::Not,
21362                                expr: Box::new(call),
21363                            }
21364                        } else {
21365                            call
21366                        };
21367                        {
21368                            return Ok(Some(expr));
21369                        }
21370                    }
21371                }
21372                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21373                // three-valued boolean tests. IS TRUE/FALSE never
21374                // return NULL, so they lower to CASE forms whose
21375                // ELSE catches the NULL branch; IS UNKNOWN on a
21376                // boolean is exactly IS NULL.
21377                if matches!(self.peek(), Token::True | Token::False)
21378                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21379                {
21380                    let tok = self.advance();
21381                    let test = match tok {
21382                        Token::True => Some(true),
21383                        Token::False => Some(false),
21384                        _ => None, // UNKNOWN
21385                    };
21386                    // v7.39 (round 328, V45) — kept as what the user
21387                    // wrote. These used to be lowered here into `CASE` /
21388                    // `IS NULL`; the semantics were right but the AST no
21389                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21390                    // was echoed back as
21391                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21392                    expr = Expr::BoolTest {
21393                        expr: Box::new(expr),
21394                        value: test,
21395                        negated,
21396                    };
21397                    {
21398                        return Ok(Some(expr));
21399                    }
21400                }
21401                if !matches!(self.peek(), Token::Null) {
21402                    return Err(self.err(format!(
21403                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21404                    if negated { " NOT" } else { "" },
21405                    self.peek()
21406                )));
21407                }
21408                self.advance();
21409                expr = Expr::IsNull {
21410                    expr: Box::new(expr),
21411                    negated,
21412                };
21413                {
21414                    return Ok(Some(expr));
21415                }
21416            }
21417        }
21418        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21419        if min_prec <= 5 {
21420            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21421            // Look one token ahead so a stray `NOT` not followed by any of
21422            // these flows through to the early return below untouched.
21423            let negated = if matches!(self.peek(), Token::Not) {
21424                let next = self.tokens.get(self.pos + 1);
21425                matches!(next, Some(Token::Between | Token::In | Token::Like))
21426                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21427                    || (self.mysql_dialect
21428                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21429                    || s.eq_ignore_ascii_case("similar"))
21430            } else {
21431                false
21432            };
21433            if negated {
21434                self.advance();
21435            }
21436            if matches!(self.peek(), Token::Between) {
21437                expr = self.parse_between_tail(expr, negated)?;
21438                {
21439                    return Ok(Some(expr));
21440                }
21441            }
21442            if matches!(self.peek(), Token::In) {
21443                if self.suppress_in_tail && !negated {
21444                    // POSITION(sub IN str) — IN belongs to the
21445                    // enclosing function syntax; stop here.
21446                    {
21447                        return Ok(None);
21448                    }
21449                }
21450                expr = self.parse_in_tail(expr, negated)?;
21451                {
21452                    return Ok(Some(expr));
21453                }
21454            }
21455            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21456            // lowers onto the internal __similar_to(expr, pat[, esc]) call
21457            // (the SQL→regex transform runs inside, in the backtracking-
21458            // friendly shape SPG's matcher needs).
21459            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21460                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21461            {
21462                self.advance(); // SIMILAR
21463                self.advance(); // TO
21464                let pattern = self.parse_expr(6)?;
21465                let mut args = alloc::vec![expr, pattern];
21466                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21467                    self.advance();
21468                    args.push(self.parse_expr(6)?);
21469                }
21470                let call = Expr::FunctionCall {
21471                    name: "__similar_to".to_string(),
21472                    args,
21473                };
21474                expr = maybe_not(call, negated);
21475                {
21476                    return Ok(Some(expr));
21477                }
21478            }
21479            if matches!(self.peek(), Token::Like) {
21480                self.advance();
21481                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21482                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21483                    expr = q;
21484                    {
21485                        return Ok(Some(expr));
21486                    }
21487                }
21488                // Pattern at the same precedence as other comparison RHSes —
21489                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21490                let mut pattern = self.parse_expr(6)?;
21491                // `ESCAPE 'c'` — rewrite a literal pattern to the
21492                // default backslash escape at parse time. Custom
21493                // escapes on non-literal patterns would need
21494                // matcher support; error honestly.
21495                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21496                    self.advance();
21497                    let esc = self.parse_expr(6)?;
21498                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21499                }
21500                expr = Expr::Like {
21501                    expr: Box::new(expr),
21502                    pattern: Box::new(pattern),
21503                    negated,
21504                    case_insensitive: false,
21505                };
21506                {
21507                    return Ok(Some(expr));
21508                }
21509            }
21510            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21511            // keyword reaches us as a plain identifier.
21512            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21513                self.advance();
21514                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21515                    expr = q;
21516                    {
21517                        return Ok(Some(expr));
21518                    }
21519                }
21520                let pattern = self.parse_expr(6)?;
21521                expr = Expr::Like {
21522                    expr: Box::new(expr),
21523                    pattern: Box::new(pattern),
21524                    negated,
21525                    case_insensitive: true,
21526                };
21527                {
21528                    return Ok(Some(expr));
21529                }
21530            }
21531            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21532            // operator (RLIKE is the alias). It is a keyword, not `~`, and
21533            // matches case-insensitively under the default collation, so it
21534            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21535            // `~*` operator uses, wrapped in NOT when negated.
21536            if self.mysql_dialect
21537                && matches!(self.peek(), Token::Ident(s)
21538                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21539            {
21540                self.advance();
21541                let pattern = self.parse_expr(6)?;
21542                let call = Expr::FunctionCall {
21543                    name: String::from("regexp_like"),
21544                    args: alloc::vec![
21545                        expr,
21546                        pattern,
21547                        Expr::Literal(Literal::String(String::from("i"))),
21548                    ],
21549                };
21550                return Ok(Some(maybe_not(call, negated)));
21551            }
21552        }
21553        let _ = expr;
21554        Ok(None)
21555    }
21556
21557    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21558        let mut lhs = self.parse_unary()?;
21559        let mut chain_len = 0usize;
21560        loop {
21561            // OPERATOR([schema.]op) reduces to its underlying
21562            // operator token before the normal dispatch.
21563            let explicit = self.peek_explicit_operator();
21564            let dispatch = match &explicit {
21565                Some((_, tok)) => self.binop_here(tok),
21566                None => self.binop_here(self.peek()),
21567            };
21568            let Some((op, prec)) = dispatch else {
21569                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21570                // of the symbol family. `binop_here` answers None for them
21571                // because they lower onto function calls rather than a
21572                // BinOp, and the fallback below reads `self.peek()` — the
21573                // word OPERATOR, not the operator. `pg_dump` writes every
21574                // catalog predicate this way, so its first query failed
21575                // and no dump ran:
21576                //
21577                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21578                //
21579                // Collapsing the wrapper to the operator it names puts the
21580                // token where the fallback already looks.
21581                if let Some((next, op_tok)) = explicit {
21582                    self.tokens.splice(self.pos..next, [op_tok]);
21583                }
21584                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21585                    lhs = e;
21586                    chain_len += 1;
21587                    if chain_len > MAX_BINARY_CHAIN {
21588                        return Err(self.err(alloc::format!(
21589                            "more than {MAX_BINARY_CHAIN} chained binary operators"
21590                        )));
21591                    }
21592                    continue;
21593                }
21594                break;
21595            };
21596            if prec < min_prec {
21597                break;
21598            }
21599            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21600            // iteratively but evaluates and drops recursively;
21601            // depth beyond the budget overflows worker stacks.
21602            chain_len += 1;
21603            if chain_len > MAX_BINARY_CHAIN {
21604                return Err(self.err(alloc::format!(
21605                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21606                )));
21607            }
21608            match explicit {
21609                Some((end_pos, _)) => self.pos = end_pos,
21610                None => {
21611                    self.advance();
21612                }
21613            }
21614            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21615            // ANY is a bare ident; ALL is a reserved Token. Both
21616            // require an immediate `(` to disambiguate from
21617            // identifier columns named `any` / `all`.
21618            let any_kind = match self.peek() {
21619                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21620                    Some(false)
21621                }
21622                Token::Ident(s) | Token::QuotedIdent(s)
21623                    if (s.eq_ignore_ascii_case("any")
21624                        || s.eq_ignore_ascii_case("some")
21625                        || s.eq_ignore_ascii_case("all"))
21626                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21627                {
21628                    Some(!s.eq_ignore_ascii_case("all"))
21629                }
21630                _ => None,
21631            };
21632            if let Some(is_any) = any_kind {
21633                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21634                continue;
21635            }
21636            let rhs = self.parse_expr(prec + 1)?;
21637            lhs = Expr::Binary {
21638                lhs: Box::new(lhs),
21639                op,
21640                rhs: Box::new(rhs),
21641            };
21642        }
21643        Ok(lhs)
21644    }
21645
21646    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21647    /// and the array form.
21648    ///
21649    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21650    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21651    /// this block's `Expr` temporaries and four `format!` sites slots in
21652    /// that frame on every level of `((((1))))`, which never reaches it.
21653    #[inline(never)]
21654    fn parse_any_all_rhs(
21655        &mut self,
21656        lhs: Expr,
21657        op: BinOp,
21658        is_any: bool,
21659    ) -> Result<Expr, ParseError> {
21660        self.advance(); // ident
21661        self.advance(); // (
21662        // `x op ANY (SELECT …)` — the quantified-subquery
21663        // form. `= ANY` is exactly IN; the other operators
21664        // lower onto EXISTS over the subquery as a derived
21665        // table, comparing against its single projection
21666        // aliased __v (x's columns resolve correlated).
21667        // ALL is the negated-EXISTS complement; a NULL
21668        // element makes PG return NULL where this lowering
21669        // returns true — the NOT NULL column case (the
21670        // practical one) is exact.
21671        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21672            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21673            // legal PG too (round-151 sibling). Out-of-line
21674            // (#[inline(never)] helper) — this sits on
21675            // parse_expr's recursive frame and the two-armed
21676            // SELECT temporary blew the nesting-budget stack.
21677            let mut sub = self.parse_any_all_select_body()?;
21678            if !matches!(self.peek(), Token::RParen) {
21679                return Err(self.err(alloc::format!(
21680                    "expected ')' after ANY/ALL subquery, got {:?}",
21681                    self.peek()
21682                )));
21683            }
21684            self.advance();
21685            if sub.items.len() != 1 {
21686                return Err(self.err(alloc::format!(
21687                    "ANY/ALL subquery must return one column, got {}",
21688                    sub.items.len()
21689                )));
21690            }
21691            if is_any && matches!(op, BinOp::Eq) {
21692                return Ok(Expr::InSubquery {
21693                    expr: Box::new(lhs),
21694                    subquery: Box::new(sub),
21695                    negated: false,
21696                });
21697            }
21698            // The engine's subquery resolvers materialise
21699            // the single-column result into an ARRAY the
21700            // existing AnyAll three-valued eval consumes.
21701            return Ok(Expr::AnyAll {
21702                expr: Box::new(lhs),
21703                op,
21704                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21705                is_any,
21706            });
21707        }
21708        let arr = self.parse_expr(0)?;
21709        if !matches!(self.peek(), Token::RParen) {
21710            return Err(self.err(alloc::format!(
21711                "expected ')' after ANY/ALL argument, got {:?}",
21712                self.peek()
21713            )));
21714        }
21715        self.advance();
21716        Ok(Expr::AnyAll {
21717            expr: Box::new(lhs),
21718            op,
21719            array: Box::new(arr),
21720            is_any,
21721        })
21722    }
21723
21724    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21725    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21726    #[inline(never)]
21727    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21728        self.advance();
21729        let e = self.parse_expr(9)?;
21730        Ok(build_center_call(e))
21731    }
21732
21733    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21734    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21735    /// unary minus.
21736    ///
21737    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21738    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21739    /// the Expr-sized local stays out of that frame.
21740    #[inline(never)]
21741    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21742        self.advance();
21743        let e = self.parse_expr(9)?;
21744        Ok(Expr::FunctionCall {
21745            name: alloc::string::String::from(name),
21746            args: alloc::vec![e],
21747        })
21748    }
21749
21750    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21751    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21752    #[inline(never)]
21753    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21754        self.advance();
21755        let e = self.parse_expr(9)?;
21756        Ok(Expr::FunctionCall {
21757            name: alloc::string::String::from(if vertical {
21758                "isvertical"
21759            } else {
21760                "ishorizontal"
21761            }),
21762            args: alloc::vec![e],
21763        })
21764    }
21765
21766    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21767    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21768    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21769    #[inline(never)]
21770    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21771        self.advance();
21772        let e = self.parse_expr(9)?;
21773        Ok(Expr::Cast {
21774            expr: Box::new(e),
21775            target: CastTarget::Named("binary".to_string()),
21776        })
21777    }
21778
21779    /// The prefix operators that share one shape: take the token, parse
21780    /// an operand at `prec`, wrap it.
21781    ///
21782    /// `#[inline(never)]`, and one function instead of five arms, for the
21783    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21784    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21785    /// debug build gives EVERY arm's locals a slot in the frame, whichever
21786    /// arm runs. `((((1))))` reaches none of these arms and was carrying
21787    /// five `Expr`-sized locals per level for them anyway.
21788    #[inline(never)]
21789    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21790        self.advance();
21791        let e = self.parse_expr(prec)?;
21792        Ok(Expr::Unary {
21793            op,
21794            expr: Box::new(e),
21795        })
21796    }
21797
21798    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21799    /// and separate from it because of the literal folding below and the
21800    /// `format!` temporaries that folding needs.
21801    #[inline(never)]
21802    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21803        self.advance();
21804        // v7.39 (round 549) — fold the sign into an integer literal that
21805        // only fits once it is negative.
21806        //
21807        // `9223372036854775808` is one past i64::MAX, so the lexer hands
21808        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21809        // folds the sign first, so `-9223372036854775808` is a bigint
21810        // there — and `-9223372036854775808 - 1` raises "bigint out of
21811        // range" where SPG quietly answered -9223372036854775809, a value
21812        // no bigint can hold. The arithmetic itself was already checked;
21813        // only the literal's type was wrong.
21814        if let Token::Numeric(lit) = self.peek()
21815            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21816        {
21817            self.advance();
21818            return Ok(Expr::Literal(Literal::Integer(folded)));
21819        }
21820        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21821        // `<->` slotted into 5 and arithmetic shifted up).
21822        let e = self.parse_expr(9)?;
21823        Ok(Expr::Unary {
21824            op: UnOp::Neg,
21825            expr: Box::new(e),
21826        })
21827    }
21828
21829    /// tsquery `!!` prefix negation, lowered to the catalog function.
21830    /// Binds like unary minus. Out-of-line for the frame reason on
21831    /// `parse_unary_op`.
21832    #[inline(never)]
21833    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21834        self.advance();
21835        let e = self.parse_expr(9)?;
21836        Ok(Expr::FunctionCall {
21837            name: String::from("tsquery_not"),
21838            args: alloc::vec![e],
21839        })
21840    }
21841
21842    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21843        match self.peek() {
21844            // NOT binds tighter than AND / XOR / OR but looser than
21845            // comparisons — its operand takes everything ≥ the comparison
21846            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21847            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21848            // was rung 3, behaviour-identical when 3 was unused; AND now
21849            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21850            Token::Not => self.parse_unary_op(UnOp::Not, 4),
21851            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21852            // The body is out-of-line: `parse_unary` is one of the three
21853            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21854            // inline arm here overflowed the native stack in
21855            // `nesting_budget_errors_cleanly` — the guard test caught it,
21856            // exactly as the eval-side cliff did in rounds 346 and 351.
21857            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21858                self.parse_binary_prefix()
21859            }
21860            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21861            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21862            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21863            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21864            Token::Minus => self.parse_prefix_minus(),
21865            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21866            // worked only because the lexer reads it as one signed literal;
21867            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21868            // PG18 and MariaDB take all of them. Binds like unary minus.
21869            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21870            // Bitwise NOT binds like unary minus.
21871            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21872            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21873            // "center of" operator; desugars to center(x). The whole arm
21874            // is out-of-line: parse_unary sits on the per-nesting-level
21875            // frame chain that MAX_NEST_DEPTH is tuned against, so no
21876            // Expr-sized local may live in this frame.
21877            Token::TsMatch => self.parse_prefix_center(),
21878            // v7.39 (round 508) — the prefix operators that are named
21879            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21880            // is length. Out-of-line for the same nesting-frame reason as
21881            // parse_prefix_center — parse_unary sits on the recursive cycle
21882            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21883            // live in this frame.
21884            Token::At => self.parse_prefix_call("abs"),
21885            Token::Hash => self.parse_prefix_call("npoints"),
21886            Token::AtMinusAt => self.parse_prefix_call("length"),
21887            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21888            // "is horizontal" (lseg / line); desugars to the existing
21889            // isvertical()/ishorizontal() functions. Out-of-line for the
21890            // same nesting-frame reason as parse_prefix_center.
21891            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21892            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21893            Token::DoubleBang => self.parse_prefix_tsquery_not(),
21894            _ => self.parse_atom(),
21895        }
21896    }
21897
21898    /// Parse a parenthesised scalar subquery body after the caller has consumed
21899    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21900    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21901    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21902    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21903    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21904    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21905    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21906    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21907    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21908    /// tips the deep-nesting test into a stack overflow).
21909    #[inline(never)]
21910    fn array_subquery_ahead(&self) -> bool {
21911        if !matches!(self.peek(), Token::LParen) {
21912            return false;
21913        }
21914        matches!(
21915            self.tokens.get(self.pos + 1),
21916            Some(Token::Select | Token::Values)
21917        ) || matches!(
21918            self.tokens.get(self.pos + 1),
21919            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21920        )
21921    }
21922
21923    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21924    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21925    /// locals stay off parse_atom's recursive frame (round 105).
21926    #[inline(never)]
21927    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21928        self.advance(); // consume `[`
21929        let mut items: Vec<Expr> = Vec::new();
21930        if !matches!(self.peek(), Token::RBracket) {
21931            loop {
21932                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21933                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21934                if matches!(self.peek(), Token::LBracket) {
21935                    items.push(self.parse_array_bracket_body()?);
21936                } else {
21937                    items.push(self.parse_expr(0)?);
21938                }
21939                match self.peek() {
21940                    Token::Comma => {
21941                        self.advance();
21942                    }
21943                    Token::RBracket => break,
21944                    other => {
21945                        return Err(self.err(alloc::format!(
21946                            "expected ',' or ']' in ARRAY literal, got {other:?}"
21947                        )));
21948                    }
21949                }
21950            }
21951        }
21952        self.advance(); // consume `]`
21953        Ok(Expr::Array(items))
21954    }
21955
21956    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21957    /// is already consumed; the current token is `(`. Desugars to a scalar
21958    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21959    /// the subquery's single-column rows in order — reusing the existing
21960    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21961    /// keeps the large `Statement` local off parse_atom's recursive frame.
21962    #[inline(never)]
21963    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21964        self.advance(); // consume `(`
21965        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21966            if w.eq_ignore_ascii_case("with"));
21967        let sub = if is_with {
21968            self.advance(); // WITH
21969            self.parse_with_cte_then_select()?
21970        } else {
21971            self.parse_select_stmt()?
21972        };
21973        if !matches!(self.peek(), Token::RParen) {
21974            return Err(self.err(alloc::format!(
21975                "expected ')' to close ARRAY(subquery), got {:?}",
21976                self.peek()
21977            )));
21978        }
21979        self.advance(); // consume `)`
21980        // Reuse the parser to build the array_agg wrapper from the subquery's
21981        // canonical text — avoids hand-constructing the derived-table AST.
21982        let wrapper = alloc::format!(
21983            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21984        );
21985        let stmt = parse_statement(&wrapper)
21986            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21987        let Statement::Select(sel) = stmt else {
21988            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21989        };
21990        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21991    }
21992
21993    #[inline(never)]
21994    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21995        let inner = if is_with {
21996            self.advance(); // WITH
21997            self.parse_with_cte_then_select()?
21998        } else {
21999            self.parse_select_stmt()?
22000        };
22001        match self.advance() {
22002            Token::RParen => {
22003                let Statement::Select(s) = inner else {
22004                    return Err(ParseError {
22005                        message: "scalar subquery body must be a SELECT".into(),
22006                        token_pos: self.consumed_pos(),
22007                    });
22008                };
22009                Ok(Expr::ScalarSubquery(Box::new(s)))
22010            }
22011            other => Err(ParseError {
22012                message: format!("expected ')' after scalar subquery, got {other:?}"),
22013                token_pos: self.consumed_pos(),
22014            }),
22015        }
22016    }
22017
22018    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22019    /// literals. The lexer splits them into an ident + string; recombine
22020    /// here. Out-of-line and returning `Option` so `parse_atom` — the
22021    /// recursive frame the 768 KiB stack budget is tuned against — pays no
22022    /// frame for the `body` / `bits` strings and their char loops (the
22023    /// round-367 frame cliff, M20).
22024    #[inline(never)]
22025    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22026        let is_hex = match self.peek() {
22027            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22028            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22029            _ => return None,
22030        };
22031        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22032            return None;
22033        }
22034        self.advance();
22035        let Token::String(body) = self.advance() else {
22036            unreachable!("guarded above");
22037        };
22038        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22039        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22040        // (hex pairs, even count required — MariaDB errors on an odd
22041        // count); `b'1010'` packs its bits big-endian, left-padded to a
22042        // byte. Lower both onto the bytea cast.
22043        if self.mysql_dialect {
22044            if is_hex {
22045                if body.len() % 2 == 1 {
22046                    return Some(Err(self.err(alloc::format!(
22047                        "invalid hex string literal X'{body}': odd digit count"
22048                    ))));
22049                }
22050                for c in body.chars() {
22051                    if !c.is_ascii_hexdigit() {
22052                        return Some(Err(
22053                            self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
22054                        ));
22055                    }
22056                }
22057                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22058            }
22059            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22060                return Some(Err(
22061                    self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
22062                ));
22063            }
22064            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22065        }
22066        let bits = if is_hex {
22067            let mut out = String::with_capacity(body.len() * 4);
22068            for c in body.chars() {
22069                let Some(d) = c.to_digit(16) else {
22070                    return Some(Err(self.err(alloc::format!(
22071                        "invalid hexadecimal digit {c:?} in X'…' bit string"
22072                    ))));
22073                };
22074                out.push_str(&alloc::format!("{d:04b}"));
22075            }
22076            out
22077        } else {
22078            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22079                return Some(Err(self.err(alloc::format!(
22080                    "invalid binary digit {bad:?} in B'…' bit string"
22081                ))));
22082            }
22083            body
22084        };
22085        // Route through the postfix-cast loop so a chained cast like
22086        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22087        // of erroring at the `::`.
22088        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22089        // literal keeps its exact length, while an explicit `::bit` cast is
22090        // bit(1) with pad/truncate semantics (PG).
22091        Some(self.finish_postfix_casts(Expr::Cast {
22092            expr: Box::new(Expr::Literal(Literal::String(bits))),
22093            target: CastTarget::Named("__bit_literal".to_string()),
22094        }))
22095    }
22096
22097    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22098        if let Some(res) = self.try_parse_bit_string_literal() {
22099            return res;
22100        }
22101        let tok_pos = self.pos;
22102        match self.advance() {
22103            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
22104            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
22105            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
22106            // carrying the source mantissa + scale so no precision is lost. A
22107            // literal too wide for i128 falls back to double precision.
22108            // Out-of-line (#[inline(never)]) — this arm sits on the
22109            // parse_expr recursion chain; its expansion locals must not
22110            // widen the recursive frame (debug frame-cliff discipline).
22111            Token::Numeric(s) => match numeric_token_to_literal(s) {
22112                Ok(lit) => Ok(Expr::Literal(lit)),
22113                Err(msg) => Err(self.err(msg)),
22114            },
22115            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
22116            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
22117            // (the lexer only emits this token in the MySQL dialect). Lower
22118            // onto the existing bytea cast; out-of-line to keep this arm off
22119            // the parse recursion frame.
22120            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22121            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22122            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22123            Token::Null => Ok(Expr::Literal(Literal::Null)),
22124            // v6.1.1 — `$N` placeholder. The actual Value lookup
22125            // happens in the engine eval path against the prepared-
22126            // statement bind buffer.
22127            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22128            Token::LParen => {
22129                // v4.10: `(SELECT ...)` in expression position is a
22130                // scalar subquery; otherwise it's a parenthesised
22131                // expression. Peek for SELECT keyword to dispatch.
22132                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22133                // lexes as Ident("with") (not a reserved token). The subquery body
22134                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22135                // so its large `Statement` local stays out of parse_atom's stack
22136                // frame — parse_atom is on the recursive `((…))` cycle and the
22137                // nesting budget is tuned to its frame size).
22138                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22139                    if s.eq_ignore_ascii_case("with"));
22140                if matches!(self.peek(), Token::Select) || is_with {
22141                    self.parse_paren_scalar_subquery(is_with)
22142                } else {
22143                    let e = self.parse_expr(0)?;
22144                    // `(a, b, …)` — a row constructor. Valid only
22145                    // in front of a comparison operator or [NOT]
22146                    // IN; both expand at parse time (lexicographic
22147                    // comparison / OR'd row equalities).
22148                    if matches!(self.peek(), Token::Comma) {
22149                        let mut row = alloc::vec![e];
22150                        while matches!(self.peek(), Token::Comma) {
22151                            self.advance();
22152                            row.push(self.parse_expr(0)?);
22153                        }
22154                        if !matches!(self.peek(), Token::RParen) {
22155                            return Err(self.err(alloc::format!(
22156                                "expected ')' after row constructor, got {:?}",
22157                                self.peek()
22158                            )));
22159                        }
22160                        self.advance();
22161                        // A bare `(a, b, …)` row constructor can carry postfix
22162                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
22163                        // early return here skips parse_atom's tail postfix
22164                        // pass, so fold casts in explicitly. For the
22165                        // comparison / predicate forms nothing postfix follows,
22166                        // so this is a no-op.
22167                        return self
22168                            .parse_row_comparison_tail(row)
22169                            .and_then(|e| self.finish_postfix_casts(e));
22170                    }
22171                    match self.advance() {
22172                        Token::RParen => Ok(e),
22173                        other => Err(ParseError {
22174                            message: format!("expected ')', got {other:?}"),
22175                            token_pos: self.consumed_pos(),
22176                        }),
22177                    }
22178                }
22179            }
22180            Token::LBracket => self.parse_vector_literal_body(),
22181            Token::Extract => self.parse_extract_atom(),
22182            Token::Interval => self.parse_interval_atom(),
22183            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
22184            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
22185            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
22186            // expression position calling the PG `left(string, n)` /
22187            // `right(string, n)` function; rebuild the AST as a regular
22188            // function call so the engine's apply_function dispatch picks
22189            // it up. Delegated to a #[inline(never)] helper so its locals
22190            // don't bloat this recursive `parse_atom` frame (the nesting
22191            // budget in `enter_nested` is tuned to parse_atom's size).
22192            Token::Left if matches!(self.peek(), Token::LParen) => {
22193                self.parse_lr_string_function_call("left")
22194            }
22195            Token::Right if matches!(self.peek(), Token::LParen) => {
22196                self.parse_lr_string_function_call("right")
22197            }
22198            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
22199            // token; we match on the bare ident. NOT is a token
22200            // (consumed in the comparison rung), but `EXISTS (...)`
22201            // at the top of an expression starts here.
22202            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22203                self.parse_exists_atom(false)
22204            }
22205            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22206            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22207            // CASE is a bare ident; we dispatch on lowercase match.
22208            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22209                self.parse_case_atom()
22210            }
22211            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22212            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22213            // '…'`. Lower onto the ::cast node so the existing
22214            // runtime text→date/timestamp paths do the parsing. The
22215            // string must follow immediately, else the ident stays a
22216            // plain column reference.
22217            Token::Ident(s)
22218                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22219                    && matches!(self.peek(), Token::String(_)) =>
22220            {
22221                let target =
22222                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22223                let Token::String(lit) = self.advance() else {
22224                    unreachable!("peek guaranteed a string token");
22225                };
22226                Ok(Expr::Cast {
22227                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22228                    target,
22229                })
22230            }
22231            // v7.39 (round 221) — the SQL-standard long spellings:
22232            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22233            // TIME ZONE '…'`. Consume the modifier and lower to the same
22234            // typed-literal cast (`timetz` / `timestamptz` for WITH).
22235            Token::Ident(s)
22236                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22237                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22238                        || w.eq_ignore_ascii_case("without"))
22239                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22240                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22241                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22242            {
22243                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22244                self.advance(); // WITH / WITHOUT
22245                self.advance(); // TIME
22246                self.advance(); // ZONE
22247                let Token::String(lit) = self.advance() else {
22248                    unreachable!("guard checked a string token");
22249                };
22250                let base = s.to_ascii_lowercase();
22251                let target = match (base.as_str(), with_tz) {
22252                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22253                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22254                    (_, true) => CastTarget::Timestamptz,
22255                    (_, false) => CastTarget::Timestamp,
22256                };
22257                Ok(Expr::Cast {
22258                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22259                    target,
22260                })
22261            }
22262            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22263            // gathers the subquery's single-column rows (in its row order)
22264            // into an array. Desugared to `array_agg` over the subquery as a
22265            // derived table; out-of-line to keep parse_atom's frame small (it
22266            // sits on the recursive nesting-budget cycle).
22267            Token::Ident(s) | Token::QuotedIdent(s)
22268                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22269            {
22270                self.parse_array_subquery()
22271            }
22272            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22273            // is not a reserved token; we match by case-insensitive
22274            // ident. The opening `[` must follow immediately. v7.39 (read01
22275            // round 105) — the body moved out-of-line so its `Vec`/loop locals
22276            // leave parse_atom's frame (which sits on the nesting-budget cycle).
22277            Token::Ident(s) | Token::QuotedIdent(s)
22278                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22279            {
22280                self.parse_array_literal_body()
22281            }
22282            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22283            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22284            // We special-case before the generic ident dispatch so
22285            // the AGAINST clause never reaches the function-call
22286            // loop (which would mis-read `(cols) AGAINST` as a
22287            // call with no trailing modifier). The shape is
22288            // rewritten to a Boolean OR over per-column
22289            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22290            // term)` so the existing FTS evaluator handles
22291            // semantics — the fulltext-GIN built at CREATE TABLE
22292            // time is currently a "real index that survives dump
22293            // round-trip"; the planner hook that actually uses
22294            // it for posting-list intersection lands in a later
22295            // sub-phase (Phase 2.2b) without touching this surface.
22296            Token::Ident(s) | Token::QuotedIdent(s)
22297                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22298            {
22299                self.parse_match_against_atom()
22300            }
22301            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22302            // v7.37.43-T4 — PG-unreserved keywords are legal column /
22303            // alias names in expression context too. `release` appears
22304            // in sentori `0003_partition_events.sql` as both a column
22305            // reference (SELECT … release …) and an INSERT column list
22306            // entry. Mirrors `expect_ident_like`'s expansion of the
22307            // identifier set.
22308            other if unreserved_keyword_text(&other).is_some() => {
22309                let s = unreserved_keyword_text(&other).unwrap();
22310                self.finish_ident_atom(s)
22311            }
22312            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22313            // only inside `SET` before, so `SELECT @@autocommit` — which
22314            // every MySQL connector asks at handshake — was a parse error.
22315            // MariaDB accepts the bare, `@@session.` and `@@global.`
22316            // spellings alike and answers from the session's own value.
22317            Token::SessionVar(v) => {
22318                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22319                // has nothing to do with a `@@` engine setting: its own
22320                // per-session namespace, and an unset one reads NULL instead
22321                // of raising. Stripping every `@` (as this did) made `@x` and
22322                // `@@x` the same node, so `SELECT @x` answered "Unknown
22323                // system variable".
22324                Ok(variable_ref_atom(&v))
22325            }
22326            other => Err(ParseError {
22327                message: format!("unexpected token {other:?} in expression"),
22328                token_pos: tok_pos,
22329            }),
22330        }
22331        // After parsing the atom, fold any postfix `::vector` casts.
22332        .and_then(|atom| self.finish_postfix_casts(atom))
22333    }
22334
22335    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22336    /// Both bind tighter than any binary op.
22337    /// Shared cast-target parser for postfix `::TYPE` and the
22338    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22339    /// If the next tokens are `( N )`, consume them and return the canonical
22340    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22341    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22342    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22343        if !matches!(self.peek(), Token::LParen) {
22344            return None;
22345        }
22346        self.advance(); // (
22347        let n = match self.advance() {
22348            Token::Integer(n) => n,
22349            _ => return Some(base.to_string()), // malformed → drop precision
22350        };
22351        if matches!(self.peek(), Token::RParen) {
22352            self.advance();
22353        }
22354        Some(alloc::format!("{base}({n})"))
22355    }
22356
22357    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22358        // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22359        // schema-qualifies every cast target, and `pg_catalog.X` names
22360        // exactly the builtin type X. Consume the qualifier and let
22361        // the ordinary target parse decide.
22362        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22363            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22364        {
22365            self.advance();
22366            self.advance();
22367        }
22368        let target = match self.advance() {
22369            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22370                "int" | "integer" | "int4" => {
22371                    if matches!(self.peek(), Token::LBracket)
22372                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22373                    {
22374                        self.advance();
22375                        self.advance();
22376                        CastTarget::IntArray
22377                    } else {
22378                        CastTarget::Int
22379                    }
22380                }
22381                "bigint" | "int8" => {
22382                    if matches!(self.peek(), Token::LBracket)
22383                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22384                    {
22385                        self.advance();
22386                        self.advance();
22387                        CastTarget::BigIntArray
22388                    } else {
22389                        CastTarget::BigInt
22390                    }
22391                }
22392                "float" | "double" => CastTarget::Float,
22393                "text" => {
22394                    // v7.10.11 — `::TEXT[]` widens to TextArray.
22395                    if matches!(self.peek(), Token::LBracket)
22396                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22397                    {
22398                        self.advance();
22399                        self.advance();
22400                        CastTarget::TextArray
22401                    } else {
22402                        CastTarget::Text
22403                    }
22404                }
22405                "bool" | "boolean" => CastTarget::Bool,
22406                "vector" => CastTarget::Vector,
22407                "date" => CastTarget::Date,
22408                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22409                // seconds precision through the Named path (the engine rounds
22410                // the sub-second field); bare `::timestamp` keeps the fast arm.
22411                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22412                    Some(named) => CastTarget::Named(named),
22413                    None => CastTarget::Timestamp,
22414                },
22415                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22416                    Some(named) => CastTarget::Named(named),
22417                    None => CastTarget::Timestamptz,
22418                },
22419                "interval" => CastTarget::Interval,
22420                "json" => CastTarget::Json,
22421                "jsonb" => CastTarget::Jsonb,
22422                // v7.39 (round 694) — these have dedicated CastTarget
22423                // variants, so they never reached the postfix `[]` handling
22424                // further down and `::regtype[]` was a SYNTAX error at the
22425                // `]`. PG has an array type for every scalar; take the
22426                // suffix here and hand the canonical `<ty>_array` name to
22427                // the engine, the same shape every other array cast uses.
22428                "regtype" if self.peek_postfix_array_brackets() => {
22429                    self.advance();
22430                    self.advance();
22431                    CastTarget::Named(alloc::string::String::from("regtype_array"))
22432                }
22433                "regclass" if self.peek_postfix_array_brackets() => {
22434                    self.advance();
22435                    self.advance();
22436                    CastTarget::Named(alloc::string::String::from("regclass_array"))
22437                }
22438                "regtype" => CastTarget::RegType,
22439                "regclass" => CastTarget::RegClass,
22440                // v7.12.0 — `::tsvector` / `::tsquery`.
22441                // Engine decodes the LHS text via the PG
22442                // external form parser.
22443                // v7.39 (round 352, M8) — MySQL's own cast targets.
22444                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22445                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22446                // such type, so they are taken only in that dialect and
22447                // fall through to the "type does not exist" arm otherwise.
22448                "signed" | "unsigned" if self.mysql_dialect => {
22449                    if matches!(self.peek(), Token::Ident(k)
22450                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22451                    {
22452                        self.advance();
22453                    }
22454                    CastTarget::Named(s.to_ascii_lowercase())
22455                }
22456                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22457                // in MySQL: MariaDB answers '123' where the SQL-standard
22458                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22459                // Truncating a number to its first digit is a wrong answer
22460                // with no error, so the MySQL session gets MySQL's reading.
22461                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22462                    CastTarget::Text
22463                }
22464                "tsvector" => CastTarget::TsVector,
22465                "tsquery" => CastTarget::TsQuery,
22466                // v7.17.0 — `::uuid`. Engine decodes the LHS
22467                // text via `spg_storage::parse_uuid_str`.
22468                "uuid" => CastTarget::Uuid,
22469                // v7.18 — `::bytea`. Engine decodes the LHS
22470                // text via the PG hex form (`'\xdeadbeef'`)
22471                // or escape form (`'\\x05\\x00'`). Closes
22472                // mailrs D-pre #3 reverse-acceptance gap.
22473                "bytea" => CastTarget::Bytea,
22474                // v7.37.5 ship triage — generic typed-cast escape.
22475                // Anything the long-tail PG type ident table knows
22476                // about(network/bit/geometry/multirange/etc.)flows
22477                // through `CastTarget::Named(canonical)`; the engine
22478                // resolves via `column_type_to_data_type` and dispatches
22479                // through the typed `coerce_value` path. Truly
22480                // unrecognised idents still hit the error arm below
22481                // because the engine rejects them.
22482                other => {
22483                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22484                    // `::varchar(255)`, etc. Capture into the canonical
22485                    // `name(p,s)` form so `type_name_to_data_type` can
22486                    // reconstruct the `DataType::Numeric { precision,
22487                    // scale }` (and similar param-carrying types).
22488                    let mut name = other.to_string();
22489                    // v7.39 (round 281) — `::bit varying(3)` is two
22490                    // words; fold the tail in so the typmod reaches the
22491                    // type resolver instead of tripping the parser.
22492                    if name.eq_ignore_ascii_case("bit")
22493                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22494                    {
22495                        self.advance();
22496                        name = alloc::string::String::from("varbit");
22497                    }
22498                    // v7.39 (round 613) — `::character varying` is the same
22499                    // two-word shape and had no fold, so the `varying` was
22500                    // left behind and the cast became a bare `character`,
22501                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22502                    // `a` where PG answers `ab`. Silently, and for a spelling
22503                    // pg_dump writes.
22504                    if name.eq_ignore_ascii_case("character")
22505                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22506                    {
22507                        self.advance();
22508                        name = alloc::string::String::from("varchar");
22509                    }
22510                    if matches!(self.peek(), Token::LParen) {
22511                        let mut buf = alloc::string::String::from("(");
22512                        let mut depth = 0usize;
22513                        loop {
22514                            match self.advance() {
22515                                Token::LParen => {
22516                                    depth += 1;
22517                                    if depth > 1 {
22518                                        buf.push('(');
22519                                    }
22520                                }
22521                                Token::RParen => {
22522                                    depth -= 1;
22523                                    if depth == 0 {
22524                                        buf.push(')');
22525                                        break;
22526                                    }
22527                                    buf.push(')');
22528                                }
22529                                Token::Comma => buf.push(','),
22530                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22531                                // v7.39 (round 273) — a minus used to fall
22532                                // into the catch-all below and vanish, so
22533                                // `::numeric(10,-2)` reached the engine as
22534                                // the text `numeric(10,2)` and silently
22535                                // rounded to two DECIMALS instead of to
22536                                // hundreds. A dropped token is not a
22537                                // no-op when it carries a sign.
22538                                Token::Minus => buf.push('-'),
22539                                Token::Eof => break,
22540                                _ => {}
22541                            }
22542                        }
22543                        name.push_str(&buf);
22544                    }
22545                    // Optional postfix `[]` widens to the array form —
22546                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22547                    // The engine's `type_name_to_data_type` recognises
22548                    // the canonical `<ty>_array` form.
22549                    if matches!(self.peek(), Token::LBracket)
22550                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22551                    {
22552                        self.advance();
22553                        self.advance();
22554                        name.push_str("_array");
22555                    }
22556                    CastTarget::Named(name)
22557                }
22558            },
22559            Token::Interval => CastTarget::Interval,
22560            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22561            // "char" (oid 18, SPG Char1 — distinct from bare `char`
22562            // = char(1)); other quoted names resolve like idents.
22563            Token::QuotedIdent(q) => {
22564                if q.eq_ignore_ascii_case("char") {
22565                    CastTarget::Named("char1".into())
22566                } else {
22567                    CastTarget::Named(q.to_ascii_lowercase())
22568                }
22569            }
22570            other => {
22571                return Err(ParseError {
22572                    message: format!("expected type ident after `::`, got {other:?}"),
22573                    token_pos: self.consumed_pos(),
22574                });
22575            }
22576        };
22577        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22578        // target to its array sibling. Closed-enum arms (Bool /
22579        // SmallInt / Numeric / Float / Date / …) didn't carry the
22580        // explicit widening that Text / Int / BigInt did, so
22581        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22582        // error. The widening here mirrors the per-arm Text /
22583        // Int / BigInt logic above + folds the new ζ-A first-class
22584        // types through `CastTarget::Named("<ty>_array")`.
22585        if matches!(self.peek(), Token::LBracket)
22586            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22587        {
22588            let widened = match &target {
22589                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22590                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22591                // v7.39 (round 326, V43) — the two temporal types stay
22592                // distinct. Both used to widen to `timestamptz_array`, so
22593                // `::timestamp[]` named the wrong target in its own error
22594                // message and lost the zone-less identity on the way.
22595                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22596                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22597                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22598                CastTarget::Json | CastTarget::Jsonb => {
22599                    Some(CastTarget::Named("jsonb_array".to_string()))
22600                }
22601                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22602                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22603                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22604                CastTarget::Named(name) => {
22605                    let mut a = name.clone();
22606                    a.push_str("_array");
22607                    Some(CastTarget::Named(a))
22608                }
22609                // Int / BigInt / Text / Vector / TsVector / TsQuery /
22610                // RegType / RegClass / TextArray / IntArray /
22611                // BigIntArray already finalised — leave as is.
22612                _ => None,
22613            };
22614            if let Some(w) = widened {
22615                self.advance();
22616                self.advance();
22617                return Ok(w);
22618            }
22619        }
22620        Ok(target)
22621    }
22622
22623    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22624        loop {
22625            // v7.38 (read01, T9) — composite field access `(expr).field`.
22626            // A bare `a.b` is consumed as a qualified column inside the ident
22627            // atom, so a Dot only survives to this postfix position when the
22628            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22629            // `.*` whole-row expansion is not handled here (projection-level).
22630            if matches!(self.peek(), Token::Dot)
22631                && matches!(
22632                    self.tokens.get(self.pos + 1),
22633                    Some(Token::Ident(_) | Token::QuotedIdent(_))
22634                )
22635            {
22636                self.advance(); // .
22637                let field = match self.advance() {
22638                    Token::Ident(s) | Token::QuotedIdent(s) => s,
22639                    other => {
22640                        return Err(
22641                            self.err(format!("expected a field name after '.', got {other:?}"))
22642                        );
22643                    }
22644                };
22645                expr = Expr::FieldAccess {
22646                    base: Box::new(expr),
22647                    field,
22648                };
22649                continue;
22650            }
22651            if matches!(self.peek(), Token::DoubleColon) {
22652                self.advance();
22653                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22654                // target set to include INTERVAL (reserved Token),
22655                // TIMESTAMPTZ, and PG catalog regtype / regclass.
22656                // mailrs follow-up H3a + H3b.
22657                let target = self.parse_cast_target()?;
22658                expr = Expr::Cast {
22659                    expr: Box::new(expr),
22660                    target,
22661                };
22662                continue;
22663            }
22664            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22665            // returns NULL for out-of-range. Multiple subscripts
22666            // chain: `a[i][j]` parses left-to-right.
22667            if matches!(self.peek(), Token::LBracket) {
22668                self.advance();
22669                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22670                // bare index stays a subscript.
22671                let lo = if matches!(self.peek(), Token::Colon) {
22672                    None
22673                } else {
22674                    Some(self.parse_expr(0)?)
22675                };
22676                if matches!(self.peek(), Token::Colon) {
22677                    self.advance();
22678                    let hi = if matches!(self.peek(), Token::RBracket) {
22679                        None
22680                    } else {
22681                        Some(Box::new(self.parse_expr(0)?))
22682                    };
22683                    if !matches!(self.peek(), Token::RBracket) {
22684                        return Err(self.err(alloc::format!(
22685                            "expected ']' after array slice, got {:?}",
22686                            self.peek()
22687                        )));
22688                    }
22689                    self.advance();
22690                    expr = Expr::ArraySlice {
22691                        target: Box::new(expr),
22692                        lo: lo.map(Box::new),
22693                        hi,
22694                    };
22695                    continue;
22696                }
22697                let index = lo.expect("non-colon branch parsed an index");
22698                if !matches!(self.peek(), Token::RBracket) {
22699                    return Err(self.err(alloc::format!(
22700                        "expected ']' after array index, got {:?}",
22701                        self.peek()
22702                    )));
22703                }
22704                self.advance();
22705                expr = Expr::ArraySubscript {
22706                    target: Box::new(expr),
22707                    index: Box::new(index),
22708                };
22709                continue;
22710            }
22711            // `expr AT TIME ZONE zone` — lowers to PG's own function
22712            // form timezone(zone, expr); the scalar implements the
22713            // offset shift (named zones error there — no tzdata).
22714            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22715                && matches!(self.tokens.get(self.pos + 1),
22716                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22717                && matches!(self.tokens.get(self.pos + 2),
22718                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22719            {
22720                self.advance(); // AT
22721                self.advance(); // TIME
22722                self.advance(); // ZONE
22723                // Zone at comparison precedence so AND/OR stay out.
22724                let zone = self.parse_expr(6)?;
22725                expr = Expr::FunctionCall {
22726                    name: "timezone".to_string(),
22727                    args: alloc::vec![zone, expr],
22728                };
22729                continue;
22730            }
22731            // `expr COLLATE "name"` — SPG's single text ordering IS
22732            // byte order, i.e. the C collation. The byte-order
22733            // spellings absorb as no-ops; a locale collation would
22734            // silently sort differently from PG, so it errors
22735            // honestly instead.
22736            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22737                self.advance();
22738                let mut cname = match self.advance() {
22739                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22740                    other => {
22741                        return Err(self.err(alloc::format!(
22742                            "expected collation name after COLLATE, got {other:?}"
22743                        )));
22744                    }
22745                };
22746                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22747                // is how `pg_dump` writes the default one:
22748                // `… COLLATE pg_catalog.default`. Reading a single token
22749                // left the SCHEMA as the name, so the clause was refused
22750                // as an unsupported locale collation and no dump ran.
22751                if matches!(self.peek(), Token::Dot) {
22752                    self.advance();
22753                    cname = match self.advance() {
22754                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22755                        // `default` lexes as a KEYWORD, and it is the name
22756                        // pg_dump writes — the same trap round 535 hit with
22757                        // TABLE / INDEX / FULL.
22758                        Token::Default => alloc::string::String::from("default"),
22759                        other => {
22760                            return Err(self.err(alloc::format!(
22761                                "expected collation name after COLLATE, got {other:?}"
22762                            )));
22763                        }
22764                    };
22765                }
22766                let lc = cname.to_ascii_lowercase();
22767                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22768                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22769                // family / `binary`) forces byte-wise, which is exactly
22770                // what `BINARY expr` does — lower onto that so every fold
22771                // site (comparison, LIKE, ORDER BY) suppresses via
22772                // `is_binary_coerced`. A `_ci` family override folds, and
22773                // under the MySQL dialect the default already folds, so it
22774                // absorbs as a no-op; likewise the C / byte-order spellings.
22775                if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22776                    expr = Expr::Cast {
22777                        expr: alloc::boxed::Box::new(expr),
22778                        target: CastTarget::Named("binary".to_string()),
22779                    };
22780                    continue;
22781                }
22782                let mysql_ci = self.mysql_dialect
22783                    && (lc.ends_with("_ci")
22784                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22785                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22786                // goes to the lowering channel, the byte-order spellings
22787                // included. Round 691 recorded only the names the old
22788                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22789                // absorbed as a no-op — and once a column could declare a
22790                // collation, absorbing the clause meant the COLUMN's
22791                // collation won where the query had asked for bytes.
22792                if self.in_order_by_key && !mysql_ci {
22793                    self.order_key_collation = Some(cname);
22794                    continue;
22795                }
22796                if !matches!(
22797                    lc.as_str(),
22798                    "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22799                ) && !mysql_ci
22800                {
22801                    // v7.38.18 — the old message read "SPG orders text
22802                    // by bytes (the C collation); locale collations are
22803                    // not supported yet", and both halves were false by
22804                    // the time it was read. This build performs locale
22805                    // collations: declared on a column or written in an
22806                    // ORDER BY key, `en_US.utf8` orders `apple, client,
22807                    // DateStyle, Zebra` exactly as PG 18.4 does. What it
22808                    // cannot do is carry a collation on an arbitrary
22809                    // expression, because there is no `Expr::Collate` to
22810                    // carry it — so say that, and say where the clause
22811                    // does work rather than telling the reader to drop it.
22812                    return Err(self.err(alloc::format!(
22813                        "COLLATE {cname:?} is not supported in this position: \
22814                         SPG carries a collation on a column declaration and \
22815                         on an ORDER BY key, not on an arbitrary expression. \
22816                         Declare it on the column (`x text COLLATE \
22817                         {cname:?}`) or move it into the ORDER BY key"
22818                    )));
22819                }
22820                continue;
22821            }
22822            return Ok(expr);
22823        }
22824    }
22825
22826    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22827    /// the first token that is not one. Schema qualifiers collapse to the
22828    /// last part, which is what every other name path here does (SPG is
22829    /// single-schema).
22830    fn take_comma_separated_names(&mut self) -> Vec<String> {
22831        let mut out = Vec::new();
22832        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22833            self.advance();
22834            let mut last = n;
22835            while matches!(self.peek(), Token::Dot) {
22836                self.advance();
22837                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22838                    last = t;
22839                }
22840            }
22841            out.push(last);
22842            if matches!(self.peek(), Token::Comma) {
22843                self.advance();
22844            } else {
22845                break;
22846            }
22847        }
22848        out
22849    }
22850
22851    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22852    ///
22853    /// The general cast-target path tests this inline; the types with their
22854    /// own `CastTarget` variant need it as a guard on their match arm,
22855    /// which is what this exists for.
22856    fn peek_postfix_array_brackets(&self) -> bool {
22857        matches!(self.peek(), Token::LBracket)
22858            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22859    }
22860
22861    /// Parse the operator tail after a `(a, b, …)` row constructor
22862    /// and expand at parse time. `=` is the conjunction of element
22863    /// equalities; `<>` its negation; the order operators expand
22864    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22865    /// equalities. Anything else (a bare row value, a subquery
22866    /// RHS) errors honestly — SPG has no composite runtime value.
22867    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22868        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22869            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22870                lhs: Box::new(l.clone()),
22871                op: BinOp::Eq,
22872                rhs: Box::new(r.clone()),
22873            });
22874            let first = it.next().expect("row has at least two elements");
22875            it.fold(first, |acc, e| Expr::Binary {
22876                lhs: Box::new(acc),
22877                op: BinOp::And,
22878                rhs: Box::new(e),
22879            })
22880        }
22881        // Lexicographic (a,b) OP (c,d):
22882        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
22883        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22884            if lhs.len() == 1 {
22885                return Expr::Binary {
22886                    lhs: Box::new(lhs[0].clone()),
22887                    op: last,
22888                    rhs: Box::new(rhs[0].clone()),
22889                };
22890            }
22891            let head_strict = Expr::Binary {
22892                lhs: Box::new(lhs[0].clone()),
22893                op: strict,
22894                rhs: Box::new(rhs[0].clone()),
22895            };
22896            let head_eq = Expr::Binary {
22897                lhs: Box::new(lhs[0].clone()),
22898                op: BinOp::Eq,
22899                rhs: Box::new(rhs[0].clone()),
22900            };
22901            Expr::Binary {
22902                lhs: Box::new(head_strict),
22903                op: BinOp::Or,
22904                rhs: Box::new(Expr::Binary {
22905                    lhs: Box::new(head_eq),
22906                    op: BinOp::And,
22907                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22908                }),
22909            }
22910        }
22911        let negated_in = if matches!(self.peek(), Token::Not)
22912            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22913        {
22914            self.advance();
22915            true
22916        } else {
22917            false
22918        };
22919        if matches!(self.peek(), Token::In) {
22920            self.advance();
22921            if !matches!(self.peek(), Token::LParen) {
22922                return Err(self.err(alloc::format!(
22923                    "expected '(' after row IN, got {:?}",
22924                    self.peek()
22925                )));
22926            }
22927            self.advance();
22928            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22929            // not a list of literal rows. Row-vs-list decomposes to
22930            // OR-of-AND above, but the subquery's rows are only known at
22931            // runtime, so keep it as a RowInSubquery node.
22932            if matches!(self.peek(), Token::Select) {
22933                let inner = self.parse_select_stmt()?;
22934                if !matches!(self.peek(), Token::RParen) {
22935                    return Err(self.err(alloc::format!(
22936                        "expected ')' after row IN-subquery, got {:?}",
22937                        self.peek()
22938                    )));
22939                }
22940                self.advance();
22941                let Statement::Select(s) = inner else {
22942                    unreachable!("parse_select_stmt always returns Statement::Select")
22943                };
22944                return Ok(Expr::RowInSubquery {
22945                    row,
22946                    subquery: Box::new(s),
22947                    negated: negated_in,
22948                });
22949            }
22950            let mut alternatives: Vec<Expr> = Vec::new();
22951            loop {
22952                // Optional ROW keyword before the paren row.
22953                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22954                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22955                {
22956                    self.advance();
22957                }
22958                if !matches!(self.peek(), Token::LParen) {
22959                    return Err(self.err(alloc::format!(
22960                        "expected '(' to open a row inside IN, got {:?}",
22961                        self.peek()
22962                    )));
22963                }
22964                self.advance();
22965                let mut rhs = alloc::vec![self.parse_expr(0)?];
22966                while matches!(self.peek(), Token::Comma) {
22967                    self.advance();
22968                    rhs.push(self.parse_expr(0)?);
22969                }
22970                if !matches!(self.peek(), Token::RParen) {
22971                    return Err(self.err(alloc::format!(
22972                        "expected ')' after row inside IN, got {:?}",
22973                        self.peek()
22974                    )));
22975                }
22976                self.advance();
22977                if rhs.len() != row.len() {
22978                    return Err(self.err(alloc::format!(
22979                        "row IN arity mismatch: left has {}, right has {}",
22980                        row.len(),
22981                        rhs.len()
22982                    )));
22983                }
22984                alternatives.push(row_eq(&row, &rhs));
22985                if matches!(self.peek(), Token::Comma) {
22986                    self.advance();
22987                    continue;
22988                }
22989                break;
22990            }
22991            if !matches!(self.peek(), Token::RParen) {
22992                return Err(self.err(alloc::format!(
22993                    "expected ')' to close row IN list, got {:?}",
22994                    self.peek()
22995                )));
22996            }
22997            self.advance();
22998            let mut it = alternatives.into_iter();
22999            let first = it.next().expect("IN list has at least one row");
23000            let combined = it.fold(first, |acc, e| Expr::Binary {
23001                lhs: Box::new(acc),
23002                op: BinOp::Or,
23003                rhs: Box::new(e),
23004            });
23005            return Ok(maybe_not(combined, negated_in));
23006        }
23007        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23008        // two periods share at least one time point. Each pair is
23009        // normalised with least/greatest (PG accepts the endpoints
23010        // in either order), then lowered to the standard
23011        // `start1 < end2 AND start2 < end1` form.
23012        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23013            if row.len() != 2 {
23014                return Err(self.err(alloc::format!(
23015                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
23016                    row.len()
23017                )));
23018            }
23019            self.advance();
23020            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23021                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23022            {
23023                self.advance();
23024            }
23025            if !matches!(self.peek(), Token::LParen) {
23026                return Err(self.err(alloc::format!(
23027                    "expected '(' after OVERLAPS, got {:?}",
23028                    self.peek()
23029                )));
23030            }
23031            self.advance();
23032            let r0 = self.parse_expr(0)?;
23033            if !matches!(self.peek(), Token::Comma) {
23034                return Err(self.err(alloc::format!(
23035                    "OVERLAPS needs (start, end) on the right, got {:?}",
23036                    self.peek()
23037                )));
23038            }
23039            self.advance();
23040            let r1 = self.parse_expr(0)?;
23041            if !matches!(self.peek(), Token::RParen) {
23042                return Err(self.err(alloc::format!(
23043                    "expected ')' after OVERLAPS pair, got {:?}",
23044                    self.peek()
23045                )));
23046            }
23047            self.advance();
23048            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
23049                name: String::from(name),
23050                args: alloc::vec![a.clone(), b.clone()],
23051            };
23052            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
23053                lhs: Box::new(lhs),
23054                op: BinOp::Lt,
23055                rhs: Box::new(rhs),
23056            };
23057            return Ok(Expr::Binary {
23058                lhs: Box::new(lt(
23059                    pair_fn("least", &row[0], &row[1]),
23060                    pair_fn("greatest", &r0, &r1),
23061                )),
23062                op: BinOp::And,
23063                rhs: Box::new(lt(
23064                    pair_fn("least", &r0, &r1),
23065                    pair_fn("greatest", &row[0], &row[1]),
23066                )),
23067            });
23068        }
23069        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
23070        // PG, `IS NULL` is true only when EVERY field is NULL, and
23071        // `IS NOT NULL` is true only when every field is non-NULL — the
23072        // latter is NOT the negation of the former (a mixed row is
23073        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
23074        // which reproduces exactly that all-fields semantics.
23075        if matches!(self.peek(), Token::Is) {
23076            self.advance();
23077            let negated = if matches!(self.peek(), Token::Not) {
23078                self.advance();
23079                true
23080            } else {
23081                false
23082            };
23083            if !matches!(self.peek(), Token::Null) {
23084                return Err(self.err(alloc::format!(
23085                    "expected NULL after row IS [NOT], got {:?}",
23086                    self.peek()
23087                )));
23088            }
23089            self.advance();
23090            let mut it = row.iter().map(|e| Expr::IsNull {
23091                expr: Box::new(e.clone()),
23092                negated,
23093            });
23094            let first = it.next().expect("row has at least two elements");
23095            return Ok(it.fold(first, |acc, e| Expr::Binary {
23096                lhs: Box::new(acc),
23097                op: BinOp::And,
23098                rhs: Box::new(e),
23099            }));
23100        }
23101        let op = match self.peek() {
23102            Token::Eq => BinOp::Eq,
23103            Token::NotEq => BinOp::NotEq,
23104            Token::Lt => BinOp::Lt,
23105            Token::LtEq => BinOp::LtEq,
23106            Token::Gt => BinOp::Gt,
23107            Token::GtEq => BinOp::GtEq,
23108            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
23109            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
23110            // constructor value, identical to the `ROW(a, b, …)` keyword form:
23111            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
23112            // (`::text`, `.field`) applies at the caller just as it does for the
23113            // ROW(...) node. All the comparison / predicate forms returned above.
23114            _ => {
23115                return Ok(Expr::FunctionCall {
23116                    name: String::from("row"),
23117                    args: row,
23118                });
23119            }
23120        };
23121        self.advance();
23122        // Optional ROW keyword before the paren row.
23123        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23124            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23125        {
23126            self.advance();
23127        }
23128        if !matches!(self.peek(), Token::LParen) {
23129            return Err(self.err(alloc::format!(
23130                "expected '(' to open the right-hand row, got {:?}",
23131                self.peek()
23132            )));
23133        }
23134        self.advance();
23135        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
23136        // subquery. Kept as a node (the subquery's row is a runtime value);
23137        // the literal-RHS form below still decomposes at parse time.
23138        if matches!(self.peek(), Token::Select) {
23139            let inner = self.parse_select_stmt()?;
23140            if !matches!(self.peek(), Token::RParen) {
23141                return Err(self.err(alloc::format!(
23142                    "expected ')' after row comparison subquery, got {:?}",
23143                    self.peek()
23144                )));
23145            }
23146            self.advance();
23147            let Statement::Select(s) = inner else {
23148                unreachable!("parse_select_stmt always returns Statement::Select")
23149            };
23150            return Ok(Expr::RowCmpSubquery {
23151                row,
23152                op,
23153                subquery: Box::new(s),
23154            });
23155        }
23156        let mut rhs = alloc::vec![self.parse_expr(0)?];
23157        while matches!(self.peek(), Token::Comma) {
23158            self.advance();
23159            rhs.push(self.parse_expr(0)?);
23160        }
23161        if !matches!(self.peek(), Token::RParen) {
23162            return Err(self.err(alloc::format!(
23163                "expected ')' after right-hand row, got {:?}",
23164                self.peek()
23165            )));
23166        }
23167        self.advance();
23168        if rhs.len() != row.len() {
23169            // v7.39 (round 239) — PG's wording (42601).
23170            return Err(self.err("unequal number of entries in row expressions".to_string()));
23171        }
23172        Ok(match op {
23173            BinOp::Eq => row_eq(&row, &rhs),
23174            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
23175            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
23176            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
23177            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
23178            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
23179            _ => unreachable!("op restricted above"),
23180        })
23181    }
23182
23183    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
23184    /// escape character becomes the matcher's default backslash:
23185    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
23186    /// → the char itself, and any pre-existing backslash escapes
23187    /// itself so it stays literal. Both operands must be string
23188    /// literals — a runtime pattern would need matcher support.
23189    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
23190        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
23191            (&pattern, &esc)
23192        else {
23193            return Err(
23194                "LIKE ... ESCAPE requires string-literal pattern and escape \
23195                 (runtime escape characters are not supported yet)"
23196                    .into(),
23197            );
23198        };
23199        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
23200        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
23201        // multi-character escape is an error.
23202        let esc_ch: Option<char> = {
23203            let mut ch_iter = e.chars();
23204            match (ch_iter.next(), ch_iter.next()) {
23205                (Some(c), None) => Some(c),
23206                (None, _) => None,
23207                (Some(_), Some(_)) => {
23208                    return Err(alloc::format!(
23209                        "ESCAPE must be a single character, got {e:?}"
23210                    ));
23211                }
23212            }
23213        };
23214        let mut out = String::with_capacity(p.len() + 4);
23215        let mut chars = p.chars();
23216        while let Some(c) = chars.next() {
23217            if Some(c) == esc_ch {
23218                match chars.next() {
23219                    // Escaped wildcard / escaped escape → keep the
23220                    // next char literal via backslash.
23221                    Some(next) => {
23222                        out.push('\\');
23223                        out.push(next);
23224                    }
23225                    None => {
23226                        return Err("LIKE pattern ends with the escape character".into());
23227                    }
23228                }
23229            } else if c == '\\' && esc_ch != Some('\\') {
23230                // A raw backslash is literal under a custom (or absent) escape
23231                // — escape it for the backslash-based matcher.
23232                out.push('\\');
23233                out.push('\\');
23234            } else {
23235                out.push(c);
23236            }
23237        }
23238        Ok(Expr::Literal(Literal::String(out)))
23239    }
23240
23241    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
23242    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
23243    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
23244    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
23245    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
23246    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
23247    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
23248    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
23249    /// array expression errors honestly rather than silently mismatching.
23250    fn try_like_any_all(
23251        &mut self,
23252        base: &Expr,
23253        negated: bool,
23254        case_insensitive: bool,
23255    ) -> Result<Option<Expr>, ParseError> {
23256        let is_any = match self.peek() {
23257            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
23258            Token::Ident(s)
23259                if s.eq_ignore_ascii_case("any")
23260                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
23261            {
23262                true
23263            }
23264            _ => return Ok(None),
23265        };
23266        self.advance(); // ANY / ALL
23267        self.advance(); // '('
23268        let arr = self.parse_expr(0)?;
23269        if !matches!(self.peek(), Token::RParen) {
23270            return Err(self.err(format!(
23271                "expected ')' after LIKE {} argument, got {:?}",
23272                if is_any { "ANY" } else { "ALL" },
23273                self.peek()
23274            )));
23275        }
23276        self.advance(); // ')'
23277        let Expr::Array(items) = arr else {
23278            return Err(self.err(
23279                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23280            ));
23281        };
23282        let mut clauses = items.into_iter().map(|p| Expr::Like {
23283            expr: Box::new(base.clone()),
23284            pattern: Box::new(p),
23285            negated,
23286            case_insensitive,
23287        });
23288        let Some(first) = clauses.next() else {
23289            // ANY(empty) = FALSE, ALL(empty) = TRUE.
23290            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23291        };
23292        let op = if is_any { BinOp::Or } else { BinOp::And };
23293        let combined = clauses.fold(first, |acc, c| Expr::Binary {
23294            lhs: Box::new(acc),
23295            op,
23296            rhs: Box::new(c),
23297        });
23298        Ok(Some(combined))
23299    }
23300
23301    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
23302    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23303    /// `AND` is not swallowed.
23304    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23305        self.advance(); // BETWEEN
23306        // SYMMETRIC — the bounds may arrive in either order; both
23307        // orientations OR together. ASYMMETRIC is the default and
23308        // absorbs as noise.
23309        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23310        {
23311            self.advance();
23312            true
23313        } else {
23314            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23315                self.advance();
23316            }
23317            false
23318        };
23319        let low = self.parse_expr(6)?;
23320        if !matches!(self.peek(), Token::And) {
23321            return Err(self.err(format!(
23322                "expected AND after BETWEEN low bound, got {:?}",
23323                self.peek()
23324            )));
23325        }
23326        self.advance();
23327        let high = self.parse_expr(6)?;
23328        let target = Box::new(expr);
23329        let range = |lo: Expr, hi: Expr| Expr::Binary {
23330            lhs: Box::new(Expr::Binary {
23331                lhs: target.clone(),
23332                op: BinOp::GtEq,
23333                rhs: Box::new(lo),
23334            }),
23335            op: BinOp::And,
23336            rhs: Box::new(Expr::Binary {
23337                lhs: target.clone(),
23338                op: BinOp::LtEq,
23339                rhs: Box::new(hi),
23340            }),
23341        };
23342        let combined = if symmetric {
23343            Expr::Binary {
23344                lhs: Box::new(range(low.clone(), high.clone())),
23345                op: BinOp::Or,
23346                rhs: Box::new(range(high, low)),
23347            }
23348        } else {
23349            range(low, high)
23350        };
23351        Ok(maybe_not(combined, negated))
23352    }
23353
23354    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
23355    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23356    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23357    /// Caller already consumed the leading `WITH` ident.
23358    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23359    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23360    /// self-reference that appears more than once in a single term.
23361    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23362        use crate::ast::{CteBody, SelectStatement};
23363        if !cte.recursive {
23364            return Ok(());
23365        }
23366        let CteBody::Select(body) = &cte.body else {
23367            return Ok(());
23368        };
23369        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23370        // check the anchor and every peer term.
23371        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23372        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23373        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23374            return Err(self.err(String::from(
23375                "ORDER BY in a recursive query is not implemented",
23376            )));
23377        }
23378        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23379            return Err(self.err(String::from(
23380                "LIMIT in a recursive query is not implemented",
23381            )));
23382        }
23383        let self_refs = |s: &SelectStatement| -> usize {
23384            let Some(from) = &s.from else {
23385                return 0;
23386            };
23387            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23388            for j in &from.joins {
23389                if j.table.name.eq_ignore_ascii_case(&cte.name) {
23390                    n += 1;
23391                }
23392            }
23393            n
23394        };
23395        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23396            return Err(self.err(alloc::format!(
23397                "recursive reference to query \"{}\" must not appear more than once",
23398                cte.name
23399            )));
23400        }
23401        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23402        // apply only when the body actually references itself (a non-self-
23403        // referencing CTE under WITH RECURSIVE may use any set-op shape).
23404        let anchor_refs = self_refs(body);
23405        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23406        if anchor_refs > 0 || union_refs {
23407            // Shape: the top level must be UNION [ALL] arms only. A self-ref
23408            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23409            // "does not have the form" error — SPG used to compute a value.
23410            if body.unions.is_empty()
23411                || body.unions.iter().any(|(k, _)| {
23412                    !matches!(
23413                        k,
23414                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23415                    )
23416                })
23417            {
23418                return Err(self.err(alloc::format!(
23419                    "recursive query \"{}\" does not have the form non-recursive-term \
23420                     UNION [ALL] recursive-term",
23421                    cte.name
23422                )));
23423            }
23424            if anchor_refs > 0 {
23425                return Err(self.err(alloc::format!(
23426                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
23427                    cte.name
23428                )));
23429            }
23430        }
23431        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23432        for (_, u) in &body.unions {
23433            if self_refs(u) == 0 {
23434                continue;
23435            }
23436            // The self-reference must not sit on the nullable side of an outer
23437            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23438            if let Some(from) = &u.from {
23439                for (i, j) in from.joins.iter().enumerate() {
23440                    let left_has_self = is_self(&from.primary)
23441                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23442                    let violated = match j.kind {
23443                        crate::ast::JoinKind::Left => is_self(&j.table),
23444                        crate::ast::JoinKind::Right => left_has_self,
23445                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23446                        _ => false,
23447                    };
23448                    if violated {
23449                        return Err(self.err(alloc::format!(
23450                            "recursive reference to query \"{}\" must not appear within an outer join",
23451                            cte.name
23452                        )));
23453                    }
23454                }
23455            }
23456            // No aggregates at the top level of the recursive term (SPG used
23457            // to run them and surface a misleading downstream error).
23458            let mut items_and_having: Vec<&Expr> = Vec::new();
23459            for it in &u.items {
23460                if let crate::ast::SelectItem::Expr { expr, .. } = it {
23461                    items_and_having.push(expr);
23462                }
23463            }
23464            if let Some(h) = &u.having {
23465                items_and_having.push(h);
23466            }
23467            for e in items_and_having {
23468                if expr_has_toplevel_aggregate(e) {
23469                    return Err(self.err(String::from(
23470                        "aggregate functions are not allowed in a recursive query's recursive term",
23471                    )));
23472                }
23473            }
23474        }
23475        // A self-reference inside a sublink expression (EXISTS / IN / scalar
23476        // subquery) anywhere in the body is rejected; a plain FROM derived
23477        // table is legal in PG and untouched here.
23478        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23479        all_terms.extend(body.unions.iter().map(|(_, u)| u));
23480        for term in all_terms {
23481            if select_has_self_ref_in_sublink(term, &cte.name) {
23482                return Err(self.err(alloc::format!(
23483                    "recursive reference to query \"{}\" must not appear within a subquery",
23484                    cte.name
23485                )));
23486            }
23487        }
23488        Ok(())
23489    }
23490
23491    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23492    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23493    /// right after parse so the engine sees a plain recursive CTE with the
23494    /// tracking columns already projected. DEPTH FIRST and CYCLE are
23495    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23496    /// text-rendered rows can't provide, and errors honestly.
23497    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23498        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23499        if cte.search.is_none() && cte.cycle.is_none() {
23500            return Ok(());
23501        }
23502        let cte_name = cte.name.clone();
23503        let col_names = cte.column_overrides.clone();
23504        if col_names.is_empty() {
23505            return Err(
23506                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23507            );
23508        }
23509        let search = cte.search.take();
23510        let cycle = cte.cycle.take();
23511        let mut extra_cols: Vec<String> = Vec::new();
23512        let col_ref = |name: &str| {
23513            Expr::Column(ColumnName {
23514                qualifier: Some(cte_name.clone()),
23515                name: name.to_string(),
23516            })
23517        };
23518        // Position of a SEARCH/CYCLE column within the CTE's column list.
23519        let pos_of = |name: &str| -> Result<usize, ParseError> {
23520            col_names
23521                .iter()
23522                .position(|c| c.eq_ignore_ascii_case(name))
23523                .ok_or_else(|| {
23524                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23525                })
23526        };
23527        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23528            let mut args = Vec::with_capacity(positions.len());
23529            for &p in positions {
23530                match items.get(p) {
23531                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23532                    _ => {
23533                        return Err(self.err(
23534                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
23535                        ));
23536                    }
23537                }
23538            }
23539            Ok(Expr::FunctionCall {
23540                name: "row".into(),
23541                args,
23542            })
23543        };
23544        let CteBody::Select(body) = &mut cte.body else {
23545            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23546        };
23547        if body.unions.is_empty() {
23548            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23549        }
23550        let rec = body.unions.len() - 1; // recursive term = last UNION peer
23551
23552        if let Some(srch) = search {
23553            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23554            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23555            // no typed `record[]`, but element-wise array ORDER BY is correct
23556            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23557            // exactly onto a typed array: DEPTH is the root→node path
23558            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23559            // orders numerically (multi-digit keys included), matching PG.
23560            //
23561            // A multi-column BY would need a record[] to keep the per-node key
23562            // tuple orderable, which SPG can't express — error honestly there
23563            // rather than mis-order.
23564            if srch.by_columns.len() != 1 {
23565                return Err(self.err(
23566                    "SEARCH … BY with multiple columns needs typed record[] ordering \
23567                     SPG doesn't have yet; a single BY column is supported"
23568                        .into(),
23569                ));
23570            }
23571            let key_pos = pos_of(&srch.by_columns[0])?;
23572            let base_key = match body.items.get(key_pos) {
23573                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23574                _ => {
23575                    return Err(
23576                        self.err("SEARCH BY column maps to a non-expression select item".into())
23577                    );
23578                }
23579            };
23580            let rec_key = match body.unions[rec].1.items.get(key_pos) {
23581                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23582                _ => {
23583                    return Err(
23584                        self.err("SEARCH BY column maps to a non-expression select item".into())
23585                    );
23586                }
23587            };
23588            if srch.depth_first {
23589                // base: ARRAY[key]; rec: array_append(cte.set, key).
23590                body.items.push(SelectItem::Expr {
23591                    expr: Expr::Array(alloc::vec![base_key]),
23592                    alias: Some(srch.set_column.clone()),
23593                });
23594                body.unions[rec].1.items.push(SelectItem::Expr {
23595                    expr: Expr::FunctionCall {
23596                        name: "array_append".into(),
23597                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
23598                    },
23599                    alias: Some(srch.set_column.clone()),
23600                });
23601            } else {
23602                // BREADTH: [depth, key]; depth starts at 0 and increments. The
23603                // leading depth element dominates the element-wise comparison,
23604                // so shallower rows sort first, then by key — PG's (depth, key).
23605                body.items.push(SelectItem::Expr {
23606                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23607                    alias: Some(srch.set_column.clone()),
23608                });
23609                // rec depth = cte.set[1] + 1.
23610                let parent_depth = Expr::ArraySubscript {
23611                    target: Box::new(col_ref(&srch.set_column)),
23612                    index: Box::new(Expr::Literal(Literal::Integer(1))),
23613                };
23614                body.unions[rec].1.items.push(SelectItem::Expr {
23615                    expr: Expr::Array(alloc::vec![
23616                        Expr::Binary {
23617                            lhs: Box::new(parent_depth),
23618                            op: BinOp::Add,
23619                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23620                        },
23621                        rec_key,
23622                    ]),
23623                    alias: Some(srch.set_column.clone()),
23624                });
23625            }
23626            extra_cols.push(srch.set_column);
23627        }
23628
23629        if let Some(cyc) = cycle {
23630            let positions: Vec<usize> = cyc
23631                .columns
23632                .iter()
23633                .map(|c| pos_of(c))
23634                .collect::<Result<_, _>>()?;
23635            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23636            // cast it to text for the cycle path: membership only needs equality,
23637            // and the record text form gives SPG a TextArray path (SPG has no
23638            // typed record[] array). Cycle detection is unaffected.
23639            let base_row = Expr::Cast {
23640                expr: Box::new(row_of(&body.items, &positions)?),
23641                target: CastTarget::Text,
23642            };
23643            let rec_row = Expr::Cast {
23644                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23645                target: CastTarget::Text,
23646            };
23647            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23648            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23649            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23650            body.items.push(SelectItem::Expr {
23651                expr: Expr::Literal(dflt.clone()),
23652                alias: Some(cyc.mark_column.clone()),
23653            });
23654            body.items.push(SelectItem::Expr {
23655                expr: Expr::Array(alloc::vec![base_row]),
23656                alias: Some(cyc.path_column.clone()),
23657            });
23658            // rec mark: ROW(cols) already in the path → cycle.
23659            let hit = Expr::AnyAll {
23660                expr: Box::new(rec_row.clone()),
23661                op: BinOp::Eq,
23662                array: Box::new(col_ref(&cyc.path_column)),
23663                is_any: true,
23664            };
23665            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23666                Expr::Case {
23667                    operand: None,
23668                    branches: alloc::vec![(hit, Expr::Literal(mark))],
23669                    else_branch: Some(Box::new(Expr::Literal(dflt))),
23670                }
23671            } else {
23672                hit
23673            };
23674            body.unions[rec].1.items.push(SelectItem::Expr {
23675                expr: mark_expr,
23676                alias: Some(cyc.mark_column.clone()),
23677            });
23678            // rec path: array_append(cte.path, ROW(cols)).
23679            body.unions[rec].1.items.push(SelectItem::Expr {
23680                expr: Expr::FunctionCall {
23681                    name: "array_append".into(),
23682                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23683                },
23684                alias: Some(cyc.path_column.clone()),
23685            });
23686            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23687            let stop = Expr::Unary {
23688                op: UnOp::Not,
23689                expr: Box::new(col_ref(&cyc.mark_column)),
23690            };
23691            let w = &mut body.unions[rec].1.where_;
23692            *w = Some(match w.take() {
23693                Some(prev) => Expr::Binary {
23694                    lhs: Box::new(prev),
23695                    op: BinOp::And,
23696                    rhs: Box::new(stop),
23697                },
23698                None => stop,
23699            });
23700            extra_cols.push(cyc.mark_column);
23701            extra_cols.push(cyc.path_column);
23702        }
23703        cte.column_overrides.extend(extra_cols);
23704        Ok(())
23705    }
23706
23707    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23708    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23709    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23710        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23711            return Ok(None);
23712        }
23713        self.advance(); // SEARCH
23714        let depth_first = match self.peek() {
23715            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23716            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23717            other => {
23718                return Err(self.err(format!(
23719                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23720                )));
23721            }
23722        };
23723        self.advance();
23724        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23725            return Err(self.err(format!(
23726                "expected FIRST after SEARCH mode, got {:?}",
23727                self.peek()
23728            )));
23729        }
23730        self.advance();
23731        if !self.peek_is_by() {
23732            return Err(self.err(format!(
23733                "expected BY after SEARCH … FIRST, got {:?}",
23734                self.peek()
23735            )));
23736        }
23737        self.advance();
23738        let mut by_columns = alloc::vec![self.expect_ident_like()?];
23739        while matches!(self.peek(), Token::Comma) {
23740            self.advance();
23741            by_columns.push(self.expect_ident_like()?);
23742        }
23743        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23744            return Err(self.err(format!(
23745                "expected SET in SEARCH clause, got {:?}",
23746                self.peek()
23747            )));
23748        }
23749        self.advance();
23750        let set_column = self.expect_ident_like()?;
23751        Ok(Some(crate::ast::SearchClause {
23752            depth_first,
23753            by_columns,
23754            set_column,
23755        }))
23756    }
23757
23758    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23759    /// USING pathcol`. Returns None when the next token isn't CYCLE.
23760    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23761        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23762            return Ok(None);
23763        }
23764        self.advance(); // CYCLE
23765        let mut columns = alloc::vec![self.expect_ident_like()?];
23766        while matches!(self.peek(), Token::Comma) {
23767            self.advance();
23768            columns.push(self.expect_ident_like()?);
23769        }
23770        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23771            return Err(self.err(format!(
23772                "expected SET in CYCLE clause, got {:?}",
23773                self.peek()
23774            )));
23775        }
23776        self.advance();
23777        let mark_column = self.expect_ident_like()?;
23778        let mut mark_value = None;
23779        let mut default_value = None;
23780        if matches!(self.peek(), Token::To) {
23781            self.advance();
23782            mark_value = Some(self.parse_cycle_literal()?);
23783            if !matches!(self.peek(), Token::Default) {
23784                return Err(self.err(format!(
23785                    "expected DEFAULT after CYCLE … TO, got {:?}",
23786                    self.peek()
23787                )));
23788            }
23789            self.advance();
23790            default_value = Some(self.parse_cycle_literal()?);
23791        }
23792        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23793            return Err(self.err(format!(
23794                "expected USING in CYCLE clause, got {:?}",
23795                self.peek()
23796            )));
23797        }
23798        self.advance();
23799        let path_column = self.expect_ident_like()?;
23800        Ok(Some(crate::ast::CycleClause {
23801            columns,
23802            mark_column,
23803            mark_value,
23804            default_value,
23805            path_column,
23806        }))
23807    }
23808
23809    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23810    /// literal (string / bool / number) in PG.
23811    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23812        match self.parse_expr(0)? {
23813            Expr::Literal(l) => Ok(l),
23814            other => Err(self.err(format!(
23815                "CYCLE mark/default value must be a literal, got {other:?}"
23816            ))),
23817        }
23818    }
23819
23820    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23821        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23822        // Comes through as an identifier; consume it if present and
23823        // mark every CTE in the clause as recursive (PG semantics —
23824        // the flag is per-WITH, not per-CTE).
23825        let mut recursive = false;
23826        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23827            && s.eq_ignore_ascii_case("recursive")
23828        {
23829            self.advance();
23830            recursive = true;
23831        }
23832        let mut ctes = Vec::new();
23833        loop {
23834            let name = self.expect_ident_like()?;
23835            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23836            // PG uses these to rename the body's output columns; we
23837            // do the same below by overriding `columns[i].name`.
23838            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23839                self.advance();
23840                let mut names = Vec::new();
23841                loop {
23842                    names.push(self.expect_ident_like()?);
23843                    if matches!(self.peek(), Token::Comma) {
23844                        self.advance();
23845                        continue;
23846                    }
23847                    break;
23848                }
23849                if !matches!(self.peek(), Token::RParen) {
23850                    return Err(self.err(format!(
23851                        "expected ')' to close CTE column list, got {:?}",
23852                        self.peek()
23853                    )));
23854                }
23855                self.advance();
23856                names
23857            } else {
23858                Vec::new()
23859            };
23860            // AS is a reserved Token::As (used by SELECT-item / FROM
23861            // aliasing) — handle it specially rather than as a bare
23862            // ident.
23863            if !matches!(self.peek(), Token::As) {
23864                return Err(self.err(format!(
23865                    "expected AS after CTE name {name:?}, got {:?}",
23866                    self.peek()
23867                )));
23868            }
23869            self.advance();
23870            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23871            // MATERIALIZED` optimizer hints. SPG materialises every
23872            // CTE, so both spellings are accepted and absorbed.
23873            if matches!(self.peek(), Token::Not) {
23874                self.advance(); // NOT
23875                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23876                    if s.eq_ignore_ascii_case("materialized"))
23877                {
23878                    self.advance();
23879                } else {
23880                    return Err(self.err(format!(
23881                        "expected MATERIALIZED after AS NOT, got {:?}",
23882                        self.peek()
23883                    )));
23884                }
23885            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23886                if s.eq_ignore_ascii_case("materialized"))
23887            {
23888                self.advance();
23889            }
23890            if !matches!(self.peek(), Token::LParen) {
23891                return Err(self.err(format!(
23892                    "expected '(' after AS in WITH clause, got {:?}",
23893                    self.peek()
23894                )));
23895            }
23896            self.advance();
23897            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23898            // RETURNING) as the CTE body in addition to SELECT.
23899            // PG writable CTE semantics. UPDATE / DELETE come in as
23900            // bare Idents (lexer keeps SELECT / INSERT as reserved
23901            // tokens but treats the rest of DML as case-insensitive
23902            // idents).
23903            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23904            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23905            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23906            let body = match self.peek() {
23907                Token::Select => {
23908                    let inner = self.parse_select_stmt()?;
23909                    let Statement::Select(s) = inner else {
23910                        unreachable!("parse_select_stmt returns Select");
23911                    };
23912                    crate::ast::CteBody::Select(s)
23913                }
23914                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23915                // `SELECT * FROM t` this way and accepts it wherever a
23916                // SELECT goes, so the CTE body dispatch needs its own
23917                // arm: this match is keyed on the FIRST token, and
23918                // `Token::Table` fell through to a tail that then
23919                // rejected what it got. `parse_table_shorthand` has
23920                // returned a desugared SelectStatement since the
23921                // shorthand landed — only the routing was missing.
23922                // Round 868 found this by putting the shorthand in a
23923                // subquery; every earlier check used a top-level form.
23924                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23925                // `SELECT * FROM t` this way and accepts it wherever a
23926                // SELECT goes, so the CTE body dispatch needs its own
23927                // arm: this match is keyed on the FIRST token, and
23928                // `Token::Table` fell through to a tail that rejected
23929                // what it got. `parse_table_shorthand` has returned a
23930                // desugared SelectStatement since the shorthand landed —
23931                // only the routing was missing, here and in the derived
23932                // table's second-token gate. Round 868 found both by
23933                // putting the shorthand in a subquery; every earlier
23934                // check had used a top-level form.
23935                Token::Table
23936                    if matches!(
23937                        self.tokens.get(self.pos + 1),
23938                        Some(Token::Ident(_) | Token::QuotedIdent(_))
23939                    ) =>
23940                {
23941                    let mut head = self.parse_table_shorthand()?;
23942                    self.parse_setop_chain_into(&mut head)?;
23943                    self.parse_select_tail_into(&mut head)?;
23944                    crate::ast::CteBody::Select(head)
23945                }
23946                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23947                // WITH t(a) AS (VALUES (1), (2)) … lowers through
23948                // the shared rows helper onto a Select body.
23949                Token::Values => {
23950                    self.advance(); // VALUES
23951                    let mut head = self.parse_values_rows_body()?;
23952                    // A VALUES seed can head a set-operation chain —
23953                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23954                    // SELECT n+1 FROM t …). Attach any trailing
23955                    // UNION / INTERSECT / EXCEPT peers so the
23956                    // recursive-CTE body parses like the SELECT seed.
23957                    self.parse_setop_chain_into(&mut head)?;
23958                    crate::ast::CteBody::Select(head)
23959                }
23960                Token::Insert => {
23961                    let inner = self.parse_one_statement()?;
23962                    let Statement::Insert(s) = inner else {
23963                        unreachable!("Token::Insert routes to Insert");
23964                    };
23965                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23966                }
23967                _ if is_update_kw => {
23968                    let inner = self.parse_one_statement()?;
23969                    let Statement::Update(s) = inner else {
23970                        return Err(
23971                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23972                        );
23973                    };
23974                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23975                }
23976                _ if is_delete_kw => {
23977                    let inner = self.parse_one_statement()?;
23978                    let Statement::Delete(s) = inner else {
23979                        return Err(
23980                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23981                        );
23982                    };
23983                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23984                }
23985                // v7.39 (round 149) — PG 17 allows MERGE as a
23986                // data-modifying CTE body.
23987                _ if is_merge_kw => {
23988                    let inner = self.parse_one_statement()?;
23989                    let Statement::Merge(s) = inner else {
23990                        return Err(
23991                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23992                        );
23993                    };
23994                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23995                }
23996                // v7.39 (round 151) — a CTE body may itself be
23997                // WITH-headed (PG grammar: PreparableStmt carries its
23998                // own with_clause). The nested statement keeps its own
23999                // ctes; the modifying-CTE-at-top-level rule is enforced
24000                // at execution.
24001                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24002                    self.advance(); // WITH
24003                    match self.parse_with_cte_then_select()? {
24004                        Statement::Select(s) => crate::ast::CteBody::Select(s),
24005                        Statement::Insert(s) => {
24006                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24007                        }
24008                        Statement::Update(s) => {
24009                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24010                        }
24011                        Statement::Delete(s) => {
24012                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24013                        }
24014                        Statement::Merge(s) => {
24015                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24016                        }
24017
24018                        other => {
24019                            return Err(self.err(format!(
24020                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24021                            )));
24022                        }
24023                    }
24024                }
24025                other => {
24026                    return Err(self.err(format!(
24027                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24028                    )));
24029                }
24030            };
24031            if !matches!(self.peek(), Token::RParen) {
24032                return Err(self.err(format!(
24033                    "expected ')' after CTE body, got {:?}",
24034                    self.peek()
24035                )));
24036            }
24037            self.advance();
24038            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
24039            // CTE, desugared into extra body columns by the engine.
24040            let search = self.parse_cte_search_clause()?;
24041            let cycle = self.parse_cte_cycle_clause()?;
24042            let mut cte = crate::ast::Cte {
24043                name,
24044                body,
24045                recursive,
24046                column_overrides,
24047                search,
24048                cycle,
24049            };
24050            self.validate_recursive_cte(&cte)?;
24051            self.desugar_cte_search_cycle(&mut cte)?;
24052            ctes.push(cte);
24053            if matches!(self.peek(), Token::Comma) {
24054                self.advance();
24055                continue;
24056            }
24057            break;
24058        }
24059        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
24060        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
24061        // the parsed CTEs to whichever statement the body produces.
24062        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24063        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24064        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24065        match self.peek() {
24066            Token::Select => {
24067                let body_stmt = self.parse_select_stmt()?;
24068                let Statement::Select(mut body) = body_stmt else {
24069                    unreachable!()
24070                };
24071                body.ctes = ctes;
24072                Ok(Statement::Select(body))
24073            }
24074            Token::Insert => {
24075                let body_stmt = self.parse_one_statement()?;
24076                let Statement::Insert(mut body) = body_stmt else {
24077                    unreachable!()
24078                };
24079                body.ctes = ctes;
24080                Ok(Statement::Insert(body))
24081            }
24082            _ if outer_is_update => {
24083                let body_stmt = self.parse_one_statement()?;
24084                let Statement::Update(mut body) = body_stmt else {
24085                    return Err(self.err(format!("expected UPDATE after WITH clause")));
24086                };
24087                body.ctes = ctes;
24088                Ok(Statement::Update(body))
24089            }
24090            _ if outer_is_delete => {
24091                let body_stmt = self.parse_one_statement()?;
24092                let Statement::Delete(mut body) = body_stmt else {
24093                    return Err(self.err(format!("expected DELETE after WITH clause")));
24094                };
24095                body.ctes = ctes;
24096                Ok(Statement::Delete(body))
24097            }
24098            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
24099            // WITH RECURSIVE is rejected with PG's exact message
24100            // (parse analysis, transformWithClause).
24101            _ if outer_is_merge => {
24102                if recursive {
24103                    return Err(self.err(String::from(
24104                        "WITH RECURSIVE is not supported for MERGE statement",
24105                    )));
24106                }
24107                let body_stmt = self.parse_one_statement()?;
24108                let Statement::Merge(mut body) = body_stmt else {
24109                    return Err(self.err(format!("expected MERGE after WITH clause")));
24110                };
24111                body.ctes = ctes;
24112                Ok(Statement::Merge(body))
24113            }
24114            other => Err(self.err(format!(
24115                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
24116            ))),
24117        }
24118    }
24119
24120    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
24121    /// already consumed the leading `EXISTS` ident via
24122    /// `self.advance()`.
24123    /// v7.13.0 — parse the rest of a `CASE … END` expression after
24124    /// the leading `CASE` ident has been consumed (mailrs round-5
24125    /// G9). Supports both the searched form
24126    /// (`CASE WHEN cond THEN val …`) and the simple form
24127    /// (`CASE operand WHEN val THEN val …`).
24128    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
24129        // Disambiguate searched vs simple form: if the next token
24130        // is `WHEN`, we're in the searched form. Otherwise the
24131        // intervening expression is the operand.
24132        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
24133            None
24134        } else {
24135            Some(Box::new(self.parse_expr(0)?))
24136        };
24137        let mut branches: Vec<(Expr, Expr)> = Vec::new();
24138        loop {
24139            match self.peek() {
24140                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
24141                    self.advance();
24142                    let cond = self.parse_expr(0)?;
24143                    match self.peek() {
24144                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
24145                            self.advance();
24146                        }
24147                        other => {
24148                            return Err(self.err(alloc::format!(
24149                                "expected THEN after CASE WHEN <expr>, got {other:?}"
24150                            )));
24151                        }
24152                    }
24153                    let value = self.parse_expr(0)?;
24154                    branches.push((cond, value));
24155                }
24156                _ => break,
24157            }
24158        }
24159        if branches.is_empty() {
24160            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
24161        }
24162        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
24163        {
24164            self.advance();
24165            Some(Box::new(self.parse_expr(0)?))
24166        } else {
24167            None
24168        };
24169        match self.peek() {
24170            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
24171                self.advance();
24172            }
24173            other => {
24174                return Err(self.err(alloc::format!(
24175                    "expected END to close CASE expression, got {other:?}"
24176                )));
24177            }
24178        }
24179        Ok(Expr::Case {
24180            operand,
24181            branches,
24182            else_branch,
24183        })
24184    }
24185
24186    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
24187    /// query-source position (EXISTS / IN / INSERT source / CTE body /
24188    /// view body). Caller consumed the WITH keyword. Only a SELECT
24189    /// outer is grammatical here; the data-modifying-CTE-at-top-level
24190    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
24191    /// maps correctly.
24192    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24193        let inner = self.parse_with_cte_then_select()?;
24194        match inner {
24195            Statement::Select(s) => Ok(s),
24196            other => Err(self.err(format!(
24197                "expected SELECT after WITH in a subquery, got {other:?}"
24198            ))),
24199        }
24200    }
24201
24202    /// True when the next token is the (unquoted) WITH keyword. WITH is
24203    /// reserved in PG, so a bare `with` can never be a column reference
24204    /// in these positions; a quoted `"with"` stays an identifier.
24205    fn peek_is_with_kw(&self) -> bool {
24206        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
24207    }
24208
24209    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
24210    /// `#[inline(never)]` keeps the large SelectStatement temporaries
24211    /// off parse_expr's recursive frame (the nesting-budget stack
24212    /// cliff — see the round-153 gate regression).
24213    #[inline(never)]
24214    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24215        if self.peek_is_with_kw() {
24216            self.advance();
24217            self.parse_nested_with_select()
24218        } else {
24219            match self.parse_select_stmt()? {
24220                Statement::Select(s) => Ok(s),
24221                other => Err(self.err(alloc::format!(
24222                    "expected SELECT inside ANY/ALL, got {other:?}"
24223                ))),
24224            }
24225        }
24226    }
24227
24228    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
24229        if !matches!(self.peek(), Token::LParen) {
24230            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
24231        }
24232        self.advance();
24233        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
24234        let s = if self.peek_is_with_kw() {
24235            self.advance();
24236            self.parse_nested_with_select()?
24237        } else {
24238            let inner = self.parse_select_stmt()?;
24239            let Statement::Select(s) = inner else {
24240                unreachable!("parse_select_stmt returns Select")
24241            };
24242            s
24243        };
24244        if !matches!(self.peek(), Token::RParen) {
24245            return Err(self.err(format!(
24246                "expected ')' after EXISTS-subquery, got {:?}",
24247                self.peek()
24248            )));
24249        }
24250        self.advance();
24251        Ok(Expr::Exists {
24252            subquery: Box::new(s),
24253            negated,
24254        })
24255    }
24256
24257    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24258        self.advance(); // IN
24259        if !matches!(self.peek(), Token::LParen) {
24260            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
24261        }
24262        self.advance();
24263        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
24264        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
24265        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
24266            let s = if self.peek_is_with_kw() {
24267                self.advance();
24268                self.parse_nested_with_select()?
24269            } else {
24270                let inner = self.parse_select_stmt()?;
24271                let Statement::Select(s) = inner else {
24272                    unreachable!("parse_select_stmt always returns Statement::Select")
24273                };
24274                s
24275            };
24276            if !matches!(self.peek(), Token::RParen) {
24277                return Err(self.err(format!(
24278                    "expected ')' after IN-subquery, got {:?}",
24279                    self.peek()
24280                )));
24281            }
24282            self.advance();
24283            return Ok(Expr::InSubquery {
24284                expr: Box::new(expr),
24285                subquery: Box::new(s),
24286                negated,
24287            });
24288        }
24289        let mut elements = Vec::new();
24290        if !matches!(self.peek(), Token::RParen) {
24291            loop {
24292                elements.push(self.parse_expr(0)?);
24293                match self.peek() {
24294                    Token::Comma => {
24295                        self.advance();
24296                    }
24297                    Token::RParen => break,
24298                    other => {
24299                        return Err(
24300                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24301                        );
24302                    }
24303                }
24304            }
24305        }
24306        self.advance(); // ')'
24307        // v7.30.2 (mailrs round-25) — flat InList node instead of a
24308        // left-deep OR-Eq chain: chain depth scaled with the element
24309        // count and overflowed the stack (eval + drop are recursive).
24310        if elements.is_empty() {
24311            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24312        }
24313        Ok(Expr::InList {
24314            expr: Box::new(expr),
24315            list: elements,
24316            negated,
24317        })
24318    }
24319
24320    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24321    /// already consumed by the caller. Elements must be numeric literals
24322    /// (with optional unary `-`); any compound expression is rejected at
24323    /// parse time so the runtime never needs to evaluate inside a vector.
24324    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24325    /// has already consumed the `EXTRACT` token before calling us —
24326    /// we pick up at the opening `(`.
24327    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24328    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24329    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24330    /// per-column OR-fold of
24331    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24332    /// term)` so the existing FTS evaluator handles semantics.
24333    ///
24334    /// The mode modifier is accepted-and-ignored at v7.17 — all
24335    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24336    /// mode operators (`+foo -bar`) would need their own parser
24337    /// (Phase 2.2c); customers who hit them today already get a
24338    /// correct lexeme-match against the bare term, only without
24339    /// the +/- precedence the customer asked for.
24340    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24341        // Already at `MATCH`-consumed position; the dispatcher
24342        // confirmed the next token is `(`.
24343        if !matches!(self.peek(), Token::LParen) {
24344            return Err(self.err(alloc::format!(
24345                "expected '(' after MATCH, got {:?}",
24346                self.peek()
24347            )));
24348        }
24349        self.advance();
24350        let mut cols: Vec<Expr> = Vec::new();
24351        loop {
24352            cols.push(self.parse_expr(0)?);
24353            match self.peek() {
24354                Token::Comma => {
24355                    self.advance();
24356                }
24357                Token::RParen => break,
24358                other => {
24359                    return Err(self.err(alloc::format!(
24360                        "expected ',' or ')' in MATCH column list, got {other:?}"
24361                    )));
24362                }
24363            }
24364        }
24365        self.advance(); // ')'
24366        // Expect AGAINST.
24367        match self.peek() {
24368            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24369                self.advance();
24370            }
24371            other => {
24372                return Err(self.err(alloc::format!(
24373                    "expected AGAINST after MATCH column list, got {other:?}"
24374                )));
24375            }
24376        }
24377        if !matches!(self.peek(), Token::LParen) {
24378            return Err(self.err(alloc::format!(
24379                "expected '(' after AGAINST, got {:?}",
24380                self.peek()
24381            )));
24382        }
24383        self.advance();
24384        // Read AGAINST's argument as a single primary token —
24385        // string literal, placeholder, or column-ref ident. We
24386        // can't call `parse_expr` / `parse_unary` here because
24387        // the postfix chain inside `parse_atom` would greedily
24388        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24389        // and fail at "expected '(' after IN". Customers always
24390        // write a literal or bound parameter in AGAINST, so this
24391        // restriction is non-blocking; the error path explains
24392        // the limit if a more complex expression shows up.
24393        let term = match self.advance() {
24394            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24395            Token::Placeholder(n) => Expr::Placeholder(n),
24396            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24397                qualifier: None,
24398                name: s,
24399            }),
24400            other => {
24401                return Err(self.err(alloc::format!(
24402                    "MATCH ... AGAINST(<term>) expects a string literal, \
24403                     bound parameter, or column ref, got {other:?}"
24404                )));
24405            }
24406        };
24407        // Optional mode tail — accept-and-ignore at v7.17:
24408        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24409        //   IN BOOLEAN MODE
24410        //   WITH QUERY EXPANSION
24411        loop {
24412            match self.peek() {
24413                // IN lexes as a reserved Token::In, not an ident,
24414                // so it gets its own arm.
24415                Token::In => {
24416                    self.advance();
24417                }
24418                Token::Ident(s) | Token::QuotedIdent(s)
24419                    if s.eq_ignore_ascii_case("natural")
24420                        || s.eq_ignore_ascii_case("language")
24421                        || s.eq_ignore_ascii_case("boolean")
24422                        || s.eq_ignore_ascii_case("mode")
24423                        || s.eq_ignore_ascii_case("with")
24424                        || s.eq_ignore_ascii_case("query")
24425                        || s.eq_ignore_ascii_case("expansion") =>
24426                {
24427                    self.advance();
24428                }
24429                _ => break,
24430            }
24431        }
24432        if !matches!(self.peek(), Token::RParen) {
24433            return Err(self.err(alloc::format!(
24434                "expected ')' to close AGAINST, got {:?}",
24435                self.peek()
24436            )));
24437        }
24438        self.advance();
24439        // Build per-column `to_tsvector('simple', col) @@
24440        // plainto_tsquery('simple', term)` and OR-fold.
24441        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24442        let plainto = Expr::FunctionCall {
24443            name: String::from("plainto_tsquery"),
24444            args: alloc::vec![simple_lit(), term.clone()],
24445        };
24446        let mut folded: Option<Expr> = None;
24447        for col in cols {
24448            let to_tsv = Expr::FunctionCall {
24449                name: String::from("to_tsvector"),
24450                args: alloc::vec![simple_lit(), col],
24451            };
24452            let leaf = Expr::Binary {
24453                lhs: Box::new(to_tsv),
24454                op: crate::ast::BinOp::TsMatch,
24455                rhs: Box::new(plainto.clone()),
24456            };
24457            folded = Some(match folded {
24458                None => leaf,
24459                Some(prev) => Expr::Binary {
24460                    lhs: Box::new(prev),
24461                    op: crate::ast::BinOp::Or,
24462                    rhs: Box::new(leaf),
24463                },
24464            });
24465        }
24466        match folded {
24467            Some(e) => Ok(e),
24468            None => Err(self.err(String::from(
24469                "MATCH(...) AGAINST(...) requires at least one column",
24470            ))),
24471        }
24472    }
24473
24474    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24475        if !matches!(self.peek(), Token::LParen) {
24476            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24477        }
24478        self.advance();
24479        let field_name = self.expect_ident_like()?;
24480        let field = match field_name.to_ascii_lowercase().as_str() {
24481            // PG accepts the plural spellings (years/months/…/millenniums) as
24482            // aliases for the singular fields — its datetime unit table has both.
24483            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24484            "year" | "years" => ExtractField::Year,
24485            "month" | "months" => ExtractField::Month,
24486            "day" | "days" => ExtractField::Day,
24487            "hour" | "hours" => ExtractField::Hour,
24488            "minute" | "minutes" => ExtractField::Minute,
24489            "second" | "seconds" => ExtractField::Second,
24490            "microsecond" | "microseconds" => ExtractField::Microsecond,
24491            "epoch" => ExtractField::Epoch,
24492            "dow" => ExtractField::Dow,
24493            "isodow" => ExtractField::Isodow,
24494            "doy" => ExtractField::Doy,
24495            "week" | "weeks" => ExtractField::Week,
24496            "isoyear" => ExtractField::Isoyear,
24497            "quarter" => ExtractField::Quarter,
24498            "decade" | "decades" => ExtractField::Decade,
24499            "century" | "centuries" => ExtractField::Century,
24500            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24501            "julian" => ExtractField::Julian,
24502            "millisecond" | "milliseconds" => ExtractField::Millisecond,
24503            "timezone" => ExtractField::Timezone,
24504            "timezone_hour" => ExtractField::TimezoneHour,
24505            "timezone_minute" => ExtractField::TimezoneMinute,
24506            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24507            // reports an unknown one with the source type (22023); carry the
24508            // raw name so eval can word it.
24509            other => ExtractField::Other(alloc::string::String::from(other)),
24510        };
24511        if !matches!(self.peek(), Token::From) {
24512            return Err(self.err(format!(
24513                "expected FROM after EXTRACT field, got {:?}",
24514                self.peek()
24515            )));
24516        }
24517        self.advance();
24518        let source = self.parse_expr(0)?;
24519        if !matches!(self.peek(), Token::RParen) {
24520            return Err(self.err(format!(
24521                "expected ')' to close EXTRACT, got {:?}",
24522                self.peek()
24523            )));
24524        }
24525        self.advance();
24526        Ok(Expr::Extract {
24527            field,
24528            source: Box::new(source),
24529        })
24530    }
24531
24532    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24533    /// is already consumed; we expect a single string literal next and
24534    /// resolve it into `Literal::Interval` at parse time so the engine
24535    /// never has to re-tokenise inside the string.
24536    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24537    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24538    /// is the SQL-standard form and is left to the path below.
24539    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24540        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24541        let (offset, sign) = match self.peek() {
24542            Token::Minus => (1, "-"),
24543            _ => (0, ""),
24544        };
24545        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24546            return None;
24547        };
24548        self.tokens
24549            .get(self.pos + offset + 1)
24550            .filter(|t| mysql_interval_unit(t).is_some())?;
24551        Some((alloc::format!("{sign}{n}"), offset + 1))
24552    }
24553
24554    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24555    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24556    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24557    ///
24558    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24559    /// this by parsing the group and then restoring `self.pos` — which could
24560    /// never have worked, because `advance()` DESTROYS the token it returns
24561    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24562    /// inert only because both branches errored back then.
24563    fn interval_paren_is_quantity(&self) -> bool {
24564        let mut depth = 0usize;
24565        let mut saw_top_level_comma = false;
24566        let mut i = self.pos;
24567        while let Some(tok) = self.tokens.get(i) {
24568            match tok {
24569                Token::LParen => depth += 1,
24570                Token::RParen => {
24571                    depth = depth.saturating_sub(1);
24572                    if depth == 0 {
24573                        return !saw_top_level_comma
24574                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24575                                .is_some();
24576                    }
24577                }
24578                // A comma directly inside the outermost parens means the
24579                // argument list of the INTERVAL() function.
24580                Token::Comma if depth == 1 => saw_top_level_comma = true,
24581                Token::Eof => return false,
24582                _ => {}
24583            }
24584            i += 1;
24585        }
24586        false
24587    }
24588
24589    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24590        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24591        // (the index of the last Ni ≤ N), distinct from the interval literal.
24592        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24593        // is decided by a non-destructive lookahead (round 422) before either
24594        // branch consumes anything. MySQL only.
24595        if self.mysql_dialect
24596            && matches!(self.peek(), Token::LParen)
24597            && !self.interval_paren_is_quantity()
24598        {
24599            self.advance(); // (
24600            let mut args = Vec::new();
24601            if !matches!(self.peek(), Token::RParen) {
24602                loop {
24603                    args.push(self.parse_expr(0)?);
24604                    if matches!(self.peek(), Token::Comma) {
24605                        self.advance();
24606                        continue;
24607                    }
24608                    break;
24609                }
24610            }
24611            if !matches!(self.peek(), Token::RParen) {
24612                return Err(self.err(alloc::format!(
24613                    "expected ')' after INTERVAL() arguments, got {:?}",
24614                    self.peek()
24615                )));
24616            }
24617            self.advance(); // )
24618            return Ok(Expr::FunctionCall {
24619                name: alloc::string::String::from("interval"),
24620                args,
24621            });
24622        }
24623        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24624        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24625        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24626        // writes every date arithmetic there is, and it did not parse at
24627        // all. PG rejects the unquoted form outright (`syntax error at or
24628        // near "1"`, measured), so it is taken only in the MySQL dialect —
24629        // PG's own `INTERVAL '1' DAY` is untouched below.
24630        if self.mysql_dialect
24631            && let Some((text, consume)) = self.peek_unquoted_interval_count()
24632        {
24633            for _ in 0..consume {
24634                self.advance(); // the optional `-` and the number
24635            }
24636            let Some(unit) = mysql_interval_unit(self.peek()) else {
24637                return Err(self.err(alloc::format!(
24638                    "expected an interval unit after INTERVAL {text}, got {:?}",
24639                    self.peek()
24640                )));
24641            };
24642            self.advance(); // the unit
24643            let (months, days, micros) = scale_mysql_interval(&text, unit)
24644                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24645            return Ok(Expr::Literal(Literal::Interval {
24646                months,
24647                days,
24648                micros,
24649                // The canonical rendering, so Display round-trips into a
24650                // form both dialects read back.
24651                text: alloc::format!("{text} {unit}"),
24652            }));
24653        }
24654        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24655        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24656        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24657        // Those cannot fold into a compile-time `Literal::Interval`, so they
24658        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24659        // builtin, which builds the value at run time (and yields NULL for a
24660        // NULL quantity, as MariaDB does). The literal path above still folds
24661        // the constant case — it is cheaper and round-trips through Display.
24662        //
24663        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24664        // MySQL's quoted spelling) keep the qualifier path below.
24665        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24666            let qty = self.parse_expr(0)?;
24667            let Some(unit) = mysql_interval_unit(self.peek()) else {
24668                return Err(self.err(alloc::format!(
24669                    "expected an interval unit after INTERVAL <expr>, got {:?}",
24670                    self.peek()
24671                )));
24672            };
24673            self.advance(); // the unit
24674            return Ok(make_interval_call(qty, unit));
24675        }
24676        let tok = self.advance();
24677        let Token::String(text) = tok else {
24678            return Err(self.err(format!(
24679                "expected string literal after INTERVAL, got {tok:?}"
24680            )));
24681        };
24682        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24683        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24684        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24685        // bare number means and the leading/trailing precision.
24686        let field1 = interval_field_of(self.peek());
24687        let qualifier = if let Some(f1) = field1 {
24688            self.advance();
24689            let f2 = if matches!(self.peek(), Token::To) {
24690                self.advance();
24691                let Some(f) = interval_field_of(self.peek()) else {
24692                    return Err(self.err(format!(
24693                        "expected an interval field after TO, got {:?}",
24694                        self.peek()
24695                    )));
24696                };
24697                self.advance();
24698                Some(f)
24699            } else {
24700                None
24701            };
24702            Some((f1, f2))
24703        } else {
24704            None
24705        };
24706        let (months, days, micros) = match qualifier {
24707            Some(q) => interpret_qualified_interval(&text, q),
24708            None => parse_interval_text(&text),
24709        }
24710        .ok_or_else(|| ParseError {
24711            message: format!(
24712                "cannot parse INTERVAL {text:?}; \
24713                     expected `<n> <unit> [<n> <unit> ...]` with units \
24714                     microsecond[s], millisecond[s], second[s], minute[s], \
24715                     hour[s], day[s], week[s], month[s], year[s]"
24716            ),
24717            token_pos: self.consumed_pos(),
24718        })?;
24719        Ok(Expr::Literal(Literal::Interval {
24720            months,
24721            days,
24722            micros,
24723            text,
24724        }))
24725    }
24726
24727    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24728    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24729    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24730    /// than a pgvector literal.
24731    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24732        self.advance(); // consume `[`
24733        let mut items: Vec<Expr> = Vec::new();
24734        if !matches!(self.peek(), Token::RBracket) {
24735            loop {
24736                if matches!(self.peek(), Token::LBracket) {
24737                    items.push(self.parse_array_bracket_body()?);
24738                } else {
24739                    items.push(self.parse_expr(0)?);
24740                }
24741                match self.peek() {
24742                    Token::Comma => {
24743                        self.advance();
24744                    }
24745                    Token::RBracket => break,
24746                    other => {
24747                        return Err(self.err(alloc::format!(
24748                            "expected ',' or ']' in array literal, got {other:?}"
24749                        )));
24750                    }
24751                }
24752            }
24753        }
24754        self.advance(); // consume `]`
24755        Ok(Expr::Array(items))
24756    }
24757
24758    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24759        let mut elems = Vec::new();
24760        if matches!(self.peek(), Token::RBracket) {
24761            self.advance();
24762            return Ok(Expr::Literal(Literal::Vector(elems)));
24763        }
24764        loop {
24765            let e = self.parse_expr(0)?;
24766            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24767                message: format!("vector element must be a numeric literal, got {e:?}"),
24768                token_pos: self.pos,
24769            })?;
24770            elems.push(x);
24771            match self.peek() {
24772                Token::Comma => {
24773                    self.advance();
24774                }
24775                Token::RBracket => {
24776                    self.advance();
24777                    break;
24778                }
24779                other => {
24780                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24781                }
24782            }
24783        }
24784        Ok(Expr::Literal(Literal::Vector(elems)))
24785    }
24786
24787    /// Atom that started with an identifier: could be `t.col`, `col`, or
24788    /// `func(arg, ...)`. Detect each shape by looking at the next token.
24789    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24790    /// [, ...])`. Caller has already consumed `OVER`. Either clause
24791    /// is optional; an empty `()` is also legal (PG semantics).
24792    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24793    /// modifier between `name(args)` and `OVER (...)`. Default is
24794    /// `Respect`. Unrecognised idents leave the stream unchanged.
24795    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24796        let Token::Ident(s) = self.peek().clone() else {
24797            return NullTreatment::Respect;
24798        };
24799        let is_ignore = s.eq_ignore_ascii_case("ignore");
24800        let is_respect = s.eq_ignore_ascii_case("respect");
24801        if !is_ignore && !is_respect {
24802            return NullTreatment::Respect;
24803        }
24804        // Lookahead for NULLS — only consume both tokens together.
24805        // pos+1 must hold a "nulls" ident.
24806        if self.pos + 1 < self.tokens.len()
24807            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24808            && s2.eq_ignore_ascii_case("nulls")
24809        {
24810            self.advance();
24811            self.advance();
24812            return if is_ignore {
24813                NullTreatment::Ignore
24814            } else {
24815                NullTreatment::Respect
24816            };
24817        }
24818        NullTreatment::Respect
24819    }
24820
24821    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24822    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24823    /// (same shape as the `OVER` tail). Consumes the whole clause and
24824    /// returns the predicate; returns `None` when no `FILTER` follows.
24825    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24826        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24827            return Ok(None);
24828        };
24829        if !s.eq_ignore_ascii_case("filter") {
24830            return Ok(None);
24831        }
24832        self.advance(); // FILTER
24833        if !matches!(self.peek(), Token::LParen) {
24834            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24835        }
24836        self.advance(); // (
24837        if !matches!(self.peek(), Token::Where) {
24838            return Err(self.err(format!(
24839                "expected WHERE inside FILTER (...), got {:?}",
24840                self.peek()
24841            )));
24842        }
24843        self.advance(); // WHERE
24844        let cond = self.parse_expr(0)?;
24845        if !matches!(self.peek(), Token::RParen) {
24846            return Err(self.err(format!(
24847                "expected ')' to close FILTER (WHERE ...), got {:?}",
24848                self.peek()
24849            )));
24850        }
24851        self.advance(); // )
24852        Ok(Some(Box::new(cond)))
24853    }
24854
24855    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24856    /// the separator as the aggregate's second argument, which is the
24857    /// shape `string_agg` already takes. Returns whether one was there.
24858    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24859        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24860            return Ok(false);
24861        }
24862        self.advance();
24863        let Token::String(sep) = self.peek().clone() else {
24864            return Err(self.err(alloc::format!(
24865                "expected a string literal after SEPARATOR, got {:?}",
24866                self.peek()
24867            )));
24868        };
24869        self.advance();
24870        args.push(Expr::Literal(Literal::String(sep)));
24871        Ok(true)
24872    }
24873
24874    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24875    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24876    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24877    /// keys, or an empty vec when no `WITHIN GROUP` follows.
24878    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24879        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24880            return Ok(Vec::new());
24881        };
24882        if !s.eq_ignore_ascii_case("within") {
24883            return Ok(Vec::new());
24884        }
24885        self.advance(); // WITHIN
24886        if !matches!(self.peek(), Token::Group) {
24887            return Err(self.err(format!(
24888                "expected GROUP after WITHIN, got {:?}",
24889                self.peek()
24890            )));
24891        }
24892        self.advance(); // GROUP
24893        if !matches!(self.peek(), Token::LParen) {
24894            return Err(self.err(format!(
24895                "expected '(' after WITHIN GROUP, got {:?}",
24896                self.peek()
24897            )));
24898        }
24899        self.advance(); // (
24900        if !matches!(self.peek(), Token::Order) {
24901            return Err(self.err(format!(
24902                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24903                self.peek()
24904            )));
24905        }
24906        self.advance(); // ORDER
24907        if !self.peek_is_by() {
24908            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24909        }
24910        self.advance(); // BY
24911        let mut keys: Vec<OrderBy> = Vec::new();
24912        loop {
24913            // v7.39 (round 691) — save/restore, the discipline this parser
24914            // already uses around `pending_sample_preds`, so a subquery inside
24915            // a key neither inherits nor leaks the channel.
24916            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24917            let saved_coll = self.order_key_collation.take();
24918            let parsed = self.parse_expr(0);
24919            self.in_order_by_key = saved_flag;
24920            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24921            let expr = parsed?;
24922            let desc = if matches!(self.peek(), Token::Desc) {
24923                self.advance();
24924                true
24925            } else if matches!(self.peek(), Token::Asc) {
24926                self.advance();
24927                false
24928            } else {
24929                false
24930            };
24931            let nulls_first = self.parse_optional_nulls_placement()?;
24932            keys.push(OrderBy {
24933                expr,
24934                desc,
24935                nulls_first,
24936                collation,
24937            });
24938            if matches!(self.peek(), Token::Comma) {
24939                self.advance();
24940            } else {
24941                break;
24942            }
24943        }
24944        if !matches!(self.peek(), Token::RParen) {
24945            return Err(self.err(format!(
24946                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24947                self.peek()
24948            )));
24949        }
24950        self.advance(); // )
24951        Ok(keys)
24952    }
24953
24954    /// No frame clause is supported.
24955    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24956    fn parse_over_clause(
24957        &mut self,
24958    ) -> Result<
24959        (
24960            Vec<Expr>,
24961            Vec<(Expr, bool, Option<bool>)>,
24962            Option<WindowFrame>,
24963        ),
24964        ParseError,
24965    > {
24966        // `OVER w` — a named-window reference. The WINDOW clause
24967        // parses after the select list, so the name rides out as a
24968        // marker in partition_by; parse_bare_select substitutes the
24969        // definition once the clause is known.
24970        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24971            let name = w.clone();
24972            self.advance();
24973            return Ok((
24974                alloc::vec![Expr::Column(crate::ast::ColumnName {
24975                    qualifier: Some("__named_window__".to_string()),
24976                    name,
24977                })],
24978                Vec::new(),
24979                None,
24980            ));
24981        }
24982        if !matches!(self.peek(), Token::LParen) {
24983            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24984        }
24985        self.advance();
24986        let mut partition_by = Vec::new();
24987        let mut order_by = Vec::new();
24988        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24989        // window, refined in place. PG's rules (probed against 18.4) differ
24990        // from the bare `OVER w1` form, so the reference rides out under its
24991        // own marker and `substitute_named_windows` applies them. The base
24992        // name is any leading identifier that isn't a window-spec keyword.
24993        let base_window = match self.peek() {
24994            Token::Ident(s) | Token::QuotedIdent(s)
24995                if !s.eq_ignore_ascii_case("partition")
24996                    && !s.eq_ignore_ascii_case("rows")
24997                    && !s.eq_ignore_ascii_case("range")
24998                    && !s.eq_ignore_ascii_case("groups") =>
24999            {
25000                let n = s.clone();
25001                self.advance();
25002                Some(n)
25003            }
25004            _ => None,
25005        };
25006        // PARTITION BY ?
25007        // v7.37.6-B promoted PARTITION to a reserved keyword
25008        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25009        // `Token::Ident("partition")`. Accept both so older sources
25010        // and the new lexer surface land on the same path.
25011        let is_partition_kw = match self.peek() {
25012            Token::Partition => true,
25013            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25014            _ => false,
25015        };
25016        if is_partition_kw {
25017            self.advance();
25018            if !self.peek_is_by() {
25019                return Err(self.err(format!(
25020                    "expected BY after PARTITION, got {:?}",
25021                    self.peek()
25022                )));
25023            }
25024            self.advance();
25025            loop {
25026                partition_by.push(self.parse_expr(0)?);
25027                if matches!(self.peek(), Token::Comma) {
25028                    self.advance();
25029                    continue;
25030                }
25031                break;
25032            }
25033        }
25034        // ORDER BY ?
25035        if matches!(self.peek(), Token::Order) {
25036            self.advance();
25037            if !self.peek_is_by() {
25038                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25039            }
25040            self.advance();
25041            loop {
25042                let e = self.parse_expr(0)?;
25043                let desc = if matches!(self.peek(), Token::Desc) {
25044                    self.advance();
25045                    true
25046                } else if matches!(self.peek(), Token::Asc) {
25047                    self.advance();
25048                    false
25049                } else {
25050                    false
25051                };
25052                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
25053                let nulls_first = self.parse_optional_nulls_placement()?;
25054                order_by.push((e, desc, nulls_first));
25055                if matches!(self.peek(), Token::Comma) {
25056                    self.advance();
25057                    continue;
25058                }
25059                break;
25060            }
25061        }
25062        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
25063        // Both keywords come through the lexer as identifiers; match
25064        // case-insensitively.
25065        let mut frame: Option<WindowFrame> = None;
25066        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
25067            let kind = if s.eq_ignore_ascii_case("rows") {
25068                Some(FrameKind::Rows)
25069            } else if s.eq_ignore_ascii_case("range") {
25070                Some(FrameKind::Range)
25071            } else if s.eq_ignore_ascii_case("groups") {
25072                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
25073                Some(FrameKind::Groups)
25074            } else {
25075                None
25076            };
25077            if let Some(kind) = kind {
25078                self.advance();
25079                frame = Some(self.parse_frame_tail(kind)?);
25080            }
25081        }
25082        if !matches!(self.peek(), Token::RParen) {
25083            return Err(self.err(format!(
25084                "expected ')' to close OVER clause, got {:?}",
25085                self.peek()
25086            )));
25087        }
25088        self.advance();
25089        if let Some(base) = base_window {
25090            // A copy may refine but never override the base's partitioning
25091            // (PG rejects it outright, before looking the name up).
25092            if !partition_by.is_empty() {
25093                return Err(self.err(alloc::format!(
25094                    "cannot override PARTITION BY clause of window \"{base}\""
25095                )));
25096            }
25097            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
25098                qualifier: Some("__named_window_ref__".to_string()),
25099                name: base,
25100            })];
25101        }
25102        Ok((partition_by, order_by, frame))
25103    }
25104
25105    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
25106    /// or `RANGE` keyword was just consumed. Accepts both
25107    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
25108    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
25109    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
25110    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
25111        let (start, end) = if matches!(self.peek(), Token::Between) {
25112            self.advance();
25113            let start = self.parse_frame_bound()?;
25114            if !matches!(self.peek(), Token::And) {
25115                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
25116            }
25117            self.advance();
25118            let end = self.parse_frame_bound()?;
25119            (start, Some(end))
25120        } else {
25121            (self.parse_frame_bound()?, None)
25122        };
25123        let exclude = self.parse_frame_exclusion()?;
25124        Ok(WindowFrame {
25125            kind,
25126            start,
25127            end,
25128            exclude,
25129        })
25130    }
25131
25132    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
25133    /// after a frame spec. NO OTHERS is the default no-op.
25134    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
25135        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
25136            return Ok(FrameExclusion::NoOthers);
25137        }
25138        self.advance(); // EXCLUDE
25139        match self.peek() {
25140            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
25141                self.advance();
25142                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
25143                    return Err(self.err(format!(
25144                        "expected ROW after EXCLUDE CURRENT, got {:?}",
25145                        self.peek()
25146                    )));
25147                }
25148                self.advance();
25149                Ok(FrameExclusion::CurrentRow)
25150            }
25151            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
25152            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
25153            // Without this arm it fell to the catch-all, whose message
25154            // self-contradictingly listed GROUP as expected.
25155            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
25156                self.advance();
25157                Ok(FrameExclusion::Group)
25158            }
25159            Token::Group => {
25160                self.advance();
25161                Ok(FrameExclusion::Group)
25162            }
25163            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
25164                self.advance();
25165                Ok(FrameExclusion::Ties)
25166            }
25167            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
25168                self.advance();
25169                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
25170                    return Err(self.err(format!(
25171                        "expected OTHERS after EXCLUDE NO, got {:?}",
25172                        self.peek()
25173                    )));
25174                }
25175                self.advance();
25176                Ok(FrameExclusion::NoOthers)
25177            }
25178            other => Err(self.err(format!(
25179                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
25180            ))),
25181        }
25182    }
25183
25184    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
25185    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
25186    /// `UNBOUNDED FOLLOWING`.
25187    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
25188        // Interval-typed offset for a value-based RANGE frame over a
25189        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
25190        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
25191        // PRECEDING`.
25192        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
25193            let dir = self.expect_ident_like()?;
25194            return if dir.eq_ignore_ascii_case("preceding") {
25195                Ok(FrameBound::IntervalPreceding {
25196                    months,
25197                    days,
25198                    micros,
25199                })
25200            } else if dir.eq_ignore_ascii_case("following") {
25201                Ok(FrameBound::IntervalFollowing {
25202                    months,
25203                    days,
25204                    micros,
25205                })
25206            } else {
25207                Err(self.err(format!(
25208                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
25209                )))
25210            };
25211        }
25212        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
25213        if let Token::Integer(n) = *self.peek() {
25214            self.advance();
25215            let n: u64 = u64::try_from(n).map_err(|_| {
25216                self.err(format!(
25217                    "invalid frame offset {n} — expected non-negative integer"
25218                ))
25219            })?;
25220            let dir = self.expect_ident_like()?;
25221            return if dir.eq_ignore_ascii_case("preceding") {
25222                Ok(FrameBound::OffsetPreceding(n))
25223            } else if dir.eq_ignore_ascii_case("following") {
25224                Ok(FrameBound::OffsetFollowing(n))
25225            } else {
25226                Err(self.err(format!(
25227                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
25228                )))
25229            };
25230        }
25231        let first = self.expect_ident_like()?;
25232        if first.eq_ignore_ascii_case("unbounded") {
25233            let dir = self.expect_ident_like()?;
25234            return if dir.eq_ignore_ascii_case("preceding") {
25235                Ok(FrameBound::UnboundedPreceding)
25236            } else if dir.eq_ignore_ascii_case("following") {
25237                Ok(FrameBound::UnboundedFollowing)
25238            } else {
25239                Err(self.err(format!(
25240                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
25241                )))
25242            };
25243        }
25244        if first.eq_ignore_ascii_case("current") {
25245            let row = self.expect_ident_like()?;
25246            if !row.eq_ignore_ascii_case("row") {
25247                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
25248            }
25249            return Ok(FrameBound::CurrentRow);
25250        }
25251        Err(self.err(format!(
25252            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
25253        )))
25254    }
25255
25256    /// Detect and consume a leading interval offset in a frame bound —
25257    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
25258    /// `(months, days, micros)`. Leaves the cursor on the trailing
25259    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
25260    /// when the next tokens are not an interval offset.
25261    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
25262        // Shape A — `INTERVAL '1 day'`.
25263        if matches!(self.peek(), Token::Interval) {
25264            self.advance(); // INTERVAL
25265            let atom = self.parse_interval_atom()?;
25266            if let Expr::Literal(Literal::Interval {
25267                months,
25268                days,
25269                micros,
25270                ..
25271            }) = atom
25272            {
25273                return Ok(Some((months, days, micros)));
25274            }
25275            return Err(self.err("expected an interval literal in frame offset".to_string()));
25276        }
25277        // Shape B — `'1 day'::interval`. Look ahead for the exact
25278        // string / `::` / interval-target triple before committing.
25279        if let Token::String(text) = self.peek() {
25280            let target_is_interval = match self.tokens.get(self.pos + 2) {
25281                Some(Token::Interval) => true,
25282                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25283                _ => false,
25284            };
25285            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25286                && target_is_interval;
25287            if is_cast {
25288                let text = text.clone();
25289                self.advance(); // string
25290                self.advance(); // ::
25291                self.advance(); // interval
25292                let parts = parse_interval_text(&text).ok_or_else(|| {
25293                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25294                })?;
25295                return Ok(Some(parts));
25296            }
25297        }
25298        Ok(None)
25299    }
25300
25301    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25302        if matches!(self.peek(), Token::Dot) {
25303            self.advance();
25304            let name = self.expect_ident_like()?;
25305            // v7.14.0 — schema-qualified function call
25306            // `<schema>.<fn>(args)`. PG dumps emit
25307            // `pg_catalog.set_config(...)` in the preamble. SPG
25308            // is single-namespace: drop the schema prefix and
25309            // route the dispatch on the bare function name.
25310            if matches!(self.peek(), Token::LParen) {
25311                return self.finish_ident_atom(name);
25312            }
25313            return Ok(Expr::Column(ColumnName {
25314                qualifier: Some(first),
25315                name,
25316            }));
25317        }
25318        if matches!(self.peek(), Token::LParen) {
25319            self.advance();
25320            // `COUNT(*)` — special-cased here because `*` isn't a normal
25321            // expression token. Lower-case match on `first` since the lexer
25322            // folds identifiers.
25323            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25324                self.advance();
25325                if !matches!(self.peek(), Token::RParen) {
25326                    return Err(self.err(format!(
25327                        "expected ')' after COUNT(*), got {:?}",
25328                        self.peek()
25329                    )));
25330                }
25331                self.advance();
25332                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25333                let filter = self.parse_filter_clause()?;
25334                // v4.12: COUNT(*) OVER (...) — same window tail.
25335                let null_treatment = self.parse_null_treatment_modifier();
25336                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25337                    && s.eq_ignore_ascii_case("over")
25338                {
25339                    self.advance();
25340                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
25341                    return Ok(Expr::WindowFunction {
25342                        name: "count_star".into(),
25343                        args: Vec::new(),
25344                        partition_by,
25345                        order_by,
25346                        frame,
25347                        null_treatment,
25348                        filter,
25349                    });
25350                }
25351                if let Some(filter) = filter {
25352                    return Ok(Expr::AggregateOrdered {
25353                        call: Box::new(Expr::FunctionCall {
25354                            name: "count_star".into(),
25355                            args: Vec::new(),
25356                        }),
25357                        order_by: Vec::new(),
25358                        distinct: false,
25359                        filter: Some(filter),
25360                    });
25361                }
25362                return Ok(Expr::FunctionCall {
25363                    name: "count_star".into(),
25364                    args: Vec::new(),
25365                });
25366            }
25367            // Function call. PG-style: zero-or-more comma-separated args.
25368            let mut args = Vec::new();
25369            // v7.38 (read01, T14) — named-argument notation `argname => value`.
25370            // Names are collected in lock-step with `args` and resolved to
25371            // positional order after the loop (the AST stays positional).
25372            let mut arg_names: Vec<Option<String>> = Vec::new();
25373            let mut agg_order_by: Vec<OrderBy> = Vec::new();
25374            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25375            // seen, so the value arguments before it can be folded.
25376            let mut saw_separator = false;
25377            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25378            // v7.32 (round-29) — accept the dual `ALL` quantifier too
25379            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25380            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25381                self.advance();
25382                true
25383            } else if matches!(self.peek(), Token::All) {
25384                self.advance();
25385                false
25386            } else {
25387                false
25388            };
25389            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25390            // TIMESTAMPDIFF take a bare unit keyword as the first
25391            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25392            // bare type keyword (DATE / TIME / DATETIME); lower them
25393            // onto string literals so the evaluator sees plain text.
25394            if ((first.eq_ignore_ascii_case("timestampadd")
25395                || first.eq_ignore_ascii_case("timestampdiff"))
25396                && matches!(self.peek(), Token::Ident(u) if matches!(
25397                    u.to_ascii_lowercase().as_str(),
25398                    "microsecond" | "second" | "minute" | "hour" | "day"
25399                        | "week" | "month" | "quarter" | "year"
25400                )))
25401                || (first.eq_ignore_ascii_case("get_format")
25402                    && matches!(self.peek(), Token::Ident(u) if matches!(
25403                        u.to_ascii_lowercase().as_str(),
25404                        "date" | "time" | "datetime" | "timestamp"
25405                    )))
25406            {
25407                if let Token::Ident(u) = self.peek() {
25408                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25409                }
25410                self.advance();
25411                if matches!(self.peek(), Token::Comma) {
25412                    self.advance();
25413                }
25414            }
25415            // `ROW(a, b, …)` keyword constructor. Followed by a
25416            // comparison operator or [NOT] IN it joins the paren
25417            // row-constructor machinery (fieldwise parse-time
25418            // expansion); bare, it stays a `row` call the evaluator
25419            // renders as PG record text.
25420            if first.eq_ignore_ascii_case("row") {
25421                let mut row_items = Vec::new();
25422                if !matches!(self.peek(), Token::RParen) {
25423                    loop {
25424                        row_items.push(self.parse_expr(0)?);
25425                        match self.peek() {
25426                            Token::Comma => {
25427                                self.advance();
25428                            }
25429                            Token::RParen => break,
25430                            other => {
25431                                return Err(self.err(format!(
25432                                    "expected ',' or ')' in ROW(...), got {other:?}"
25433                                )));
25434                            }
25435                        }
25436                    }
25437                }
25438                self.advance(); // ')'
25439                let comparison_follows = matches!(
25440                    self.peek(),
25441                    Token::Eq
25442                        | Token::NotEq
25443                        | Token::Lt
25444                        | Token::LtEq
25445                        | Token::Gt
25446                        | Token::GtEq
25447                        | Token::In
25448                ) || (matches!(self.peek(), Token::Not)
25449                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25450                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25451                if comparison_follows && !row_items.is_empty() {
25452                    return self.parse_row_comparison_tail(row_items);
25453                }
25454                return Ok(Expr::FunctionCall {
25455                    name: String::from("row"),
25456                    args: row_items,
25457                });
25458            }
25459            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25460            // the parse-mode keyword introduces the source text. SPG
25461            // carries XML as text, so both modes lower to __xmlparse(expr)
25462            // which validates well-formedness and returns Value::Xml.
25463            if first.eq_ignore_ascii_case("xmlparse")
25464                && matches!(self.peek(), Token::Ident(kw)
25465                    if kw.eq_ignore_ascii_case("document")
25466                        || kw.eq_ignore_ascii_case("content"))
25467            {
25468                let mode = match self.advance() {
25469                    Token::Ident(kw) => kw.to_ascii_lowercase(),
25470                    _ => unreachable!("peeked an ident"),
25471                };
25472                let src = self.parse_expr(0)?;
25473                if !matches!(self.peek(), Token::RParen) {
25474                    return Err(self.err(format!(
25475                        "expected ')' to close XMLPARSE, got {:?}",
25476                        self.peek()
25477                    )));
25478                }
25479                self.advance();
25480                return Ok(Expr::FunctionCall {
25481                    name: String::from("__xmlparse"),
25482                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25483                });
25484            }
25485            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25486            // keyword introduces the element name (a bare or quoted
25487            // identifier), then optional content expressions. Lower to a
25488            // plain `xmlelement(name_text, content …)` call.
25489            if first.eq_ignore_ascii_case("xmlelement")
25490                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25491            {
25492                self.advance(); // consume NAME
25493                let elem_name = match self.peek().clone() {
25494                    Token::Ident(n) | Token::QuotedIdent(n) => {
25495                        self.advance();
25496                        n
25497                    }
25498                    other => {
25499                        return Err(self.err(format!(
25500                            "expected element name after XMLELEMENT NAME, got {other:?}"
25501                        )));
25502                    }
25503                };
25504                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25505                while matches!(self.peek(), Token::Comma) {
25506                    self.advance();
25507                    args.push(self.parse_expr(0)?);
25508                }
25509                if !matches!(self.peek(), Token::RParen) {
25510                    return Err(self.err(format!(
25511                        "expected ')' to close XMLELEMENT, got {:?}",
25512                        self.peek()
25513                    )));
25514                }
25515                self.advance();
25516                return Ok(Expr::FunctionCall {
25517                    name: String::from("xmlelement"),
25518                    args,
25519                });
25520            }
25521            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25522            // becomes a `<name>value</name>` element; a bare column infers its
25523            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25524            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25525                let mut args: Vec<Expr> = Vec::new();
25526                loop {
25527                    let val = self.parse_expr(0)?;
25528                    let name = if matches!(self.peek(), Token::As) {
25529                        self.advance();
25530                        match self.peek().clone() {
25531                            Token::Ident(n) | Token::QuotedIdent(n) => {
25532                                self.advance();
25533                                n
25534                            }
25535                            other => {
25536                                return Err(self.err(format!(
25537                                    "expected name after AS in XMLFOREST, got {other:?}"
25538                                )));
25539                            }
25540                        }
25541                    } else if let Expr::Column(c) = &val {
25542                        c.name.clone()
25543                    } else {
25544                        return Err(
25545                            self.err("XMLFOREST element without a column name needs AS".into())
25546                        );
25547                    };
25548                    args.push(Expr::Literal(Literal::String(name)));
25549                    args.push(val);
25550                    if matches!(self.peek(), Token::Comma) {
25551                        self.advance();
25552                    } else {
25553                        break;
25554                    }
25555                }
25556                if !matches!(self.peek(), Token::RParen) {
25557                    return Err(self.err(format!(
25558                        "expected ')' to close XMLFOREST, got {:?}",
25559                        self.peek()
25560                    )));
25561                }
25562                self.advance();
25563                return Ok(Expr::FunctionCall {
25564                    name: String::from("xmlforest"),
25565                    args,
25566                });
25567            }
25568            // SQL-standard `POSITION(sub IN str)` — lowers onto
25569            // strpos(str, sub). IN is the argument separator here,
25570            // so the needle parses with the IN-tail suppressed.
25571            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25572                let saved = self.suppress_in_tail;
25573                self.suppress_in_tail = true;
25574                let needle = self.parse_expr(0);
25575                self.suppress_in_tail = saved;
25576                let needle = needle?;
25577                if matches!(self.peek(), Token::In) {
25578                    self.advance();
25579                    let haystack = self.parse_expr(0)?;
25580                    if !matches!(self.peek(), Token::RParen) {
25581                        return Err(self.err(format!(
25582                            "expected ')' to close POSITION, got {:?}",
25583                            self.peek()
25584                        )));
25585                    }
25586                    self.advance();
25587                    return Ok(Expr::FunctionCall {
25588                        name: String::from("strpos"),
25589                        args: alloc::vec![haystack, needle],
25590                    });
25591                }
25592                // position(sub, str) comma form (incl. bytea) —
25593                // hand the parsed first arg to the generic list.
25594                args.push(needle);
25595                if matches!(self.peek(), Token::Comma) {
25596                    self.advance();
25597                }
25598            }
25599            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25600            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25601            // plain comma forms TRIM(str) / TRIM(str, chars) keep
25602            // riding the generic argument list below.
25603            if first.eq_ignore_ascii_case("trim") {
25604                let mode = match self.peek() {
25605                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25606                        self.advance();
25607                        Some("btrim")
25608                    }
25609                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25610                        self.advance();
25611                        Some("ltrim")
25612                    }
25613                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25614                        self.advance();
25615                        Some("rtrim")
25616                    }
25617                    _ => None,
25618                };
25619                if mode.is_some() || matches!(self.peek(), Token::From) {
25620                    // TRIM([mode] FROM str) — no strip-chars.
25621                    let chars = if matches!(self.peek(), Token::From) {
25622                        None
25623                    } else {
25624                        Some(self.parse_expr(0)?)
25625                    };
25626                    if !matches!(self.peek(), Token::From) {
25627                        return Err(self.err(format!(
25628                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25629                            self.peek()
25630                        )));
25631                    }
25632                    self.advance();
25633                    let target = self.parse_expr(0)?;
25634                    if !matches!(self.peek(), Token::RParen) {
25635                        return Err(
25636                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25637                        );
25638                    }
25639                    self.advance();
25640                    let mut trim_args = alloc::vec![target];
25641                    if let Some(c) = chars {
25642                        trim_args.push(c);
25643                    }
25644                    return Ok(Expr::FunctionCall {
25645                        name: String::from(mode.unwrap_or("btrim")),
25646                        args: trim_args,
25647                    });
25648                }
25649            }
25650            if !matches!(self.peek(), Token::RParen) {
25651                loop {
25652                    // v7.38 (read01, T14) — `argname => value` names this arg.
25653                    // v7.39 (read01 round 77) — `argname := value` is the same
25654                    // thing, and it is the spelling PG's own docs lead with. It
25655                    // was simply never lexed here, so every `f(x := 1)` died in
25656                    // the parser regardless of what `f` was.
25657                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25658                        (
25659                            Token::Ident(n) | Token::QuotedIdent(n),
25660                            Some(Token::FatArrow | Token::ColonEq),
25661                        ) => {
25662                            let name = n.clone();
25663                            self.advance(); // name
25664                            self.advance(); // => / :=
25665                            Some(name)
25666                        }
25667                        _ => None,
25668                    };
25669                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25670                    // array's elements into a variadic call's trailing args
25671                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25672                    // reserved, so it arrives as a bare ident before the arg.
25673                    let is_variadic = this_name.is_none()
25674                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25675                    if is_variadic {
25676                        self.advance();
25677                    }
25678                    let arg = self.parse_expr(0)?;
25679                    args.push(match &this_name {
25680                        // The callee's parameter names decide the slot, and a
25681                        // user function's live in the catalog. Carry the name
25682                        // to eval rather than guessing here.
25683                        Some(n) => Expr::NamedArg {
25684                            name: n.clone(),
25685                            expr: Box::new(arg),
25686                        },
25687                        None if is_variadic => Expr::Variadic(Box::new(arg)),
25688                        None => arg,
25689                    });
25690                    arg_names.push(this_name);
25691                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
25692                    // The `::` cast already worked; this lowers the
25693                    // function form onto the same Expr::Cast node.
25694                    if first.eq_ignore_ascii_case("cast")
25695                        && args.len() == 1
25696                        && matches!(self.peek(), Token::As)
25697                    {
25698                        self.advance();
25699                        let target = self.parse_cast_target()?;
25700                        if !matches!(self.peek(), Token::RParen) {
25701                            return Err(self.err(format!(
25702                                "expected ')' to close CAST, got {:?}",
25703                                self.peek()
25704                            )));
25705                        }
25706                        self.advance();
25707                        return Ok(Expr::Cast {
25708                            expr: Box::new(args.pop().expect("one arg")),
25709                            target,
25710                        });
25711                    }
25712                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25713                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25714                    // keywords; SPG's lexer makes them plain idents (so they'd be
25715                    // read as column refs). Lower the keyword to the string form
25716                    // the evaluator already accepts.
25717                    if first.eq_ignore_ascii_case("normalize")
25718                        && args.len() == 1
25719                        && matches!(self.peek(), Token::Comma)
25720                    {
25721                        let form = match self.tokens.get(self.pos + 1) {
25722                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25723                                let up = f.to_ascii_uppercase();
25724                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25725                            }
25726                            _ => None,
25727                        };
25728                        if let Some(up) = form {
25729                            self.advance(); // comma
25730                            self.advance(); // form keyword
25731                            args.push(Expr::Literal(Literal::String(up)));
25732                        }
25733                    }
25734                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25735                    // form. Desugars to the comma-list shape evaluator already
25736                    // handles. Triggered after the first arg when the function
25737                    // name is substring / substr and the next token is FROM
25738                    // (a reserved keyword in PG; SPG also reserves it).
25739                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25740                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25741                    // internal __substring_similar(str, pat, esc) call.
25742                    if (first.eq_ignore_ascii_case("substring")
25743                        || first.eq_ignore_ascii_case("substr"))
25744                        && args.len() == 1
25745                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25746                    {
25747                        self.advance(); // SIMILAR
25748                        let pattern = self.parse_expr(0)?;
25749                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25750                        {
25751                            return Err(self.err(format!(
25752                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25753                                self.peek()
25754                            )));
25755                        }
25756                        self.advance(); // ESCAPE
25757                        let esc = self.parse_expr(0)?;
25758                        if !matches!(self.peek(), Token::RParen) {
25759                            return Err(self.err(format!(
25760                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
25761                                self.peek()
25762                            )));
25763                        }
25764                        self.advance();
25765                        args.push(pattern);
25766                        args.push(esc);
25767                        return Ok(Expr::FunctionCall {
25768                            name: "__substring_similar".to_string(),
25769                            args,
25770                        });
25771                    }
25772                    if (first.eq_ignore_ascii_case("substring")
25773                        || first.eq_ignore_ascii_case("substr"))
25774                        && args.len() == 1
25775                        && matches!(self.peek(), Token::From | Token::For)
25776                    {
25777                        // `substring(str FROM pos [FOR len])`, or the FOR-only
25778                        // `substring(str FOR len)` which PG treats as FROM 1.
25779                        if matches!(self.peek(), Token::From) {
25780                            self.advance();
25781                            let start = self.parse_expr(0)?;
25782                            args.push(start);
25783                        } else {
25784                            args.push(Expr::Literal(Literal::Integer(1)));
25785                        }
25786                        if matches!(self.peek(), Token::For) {
25787                            self.advance();
25788                            let length = self.parse_expr(0)?;
25789                            args.push(length);
25790                        }
25791                        if !matches!(self.peek(), Token::RParen) {
25792                            return Err(self.err(format!(
25793                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25794                                self.peek()
25795                            )));
25796                        }
25797                        self.advance();
25798                        return Ok(Expr::FunctionCall {
25799                            name: first.to_ascii_lowercase(),
25800                            args,
25801                        });
25802                    }
25803                    // PG `overlay(str PLACING repl FROM n [FOR len])`
25804                    // syntactic form. Desugars to the `overlay(str,
25805                    // repl, n[, len])` comma-list shape the evaluator
25806                    // already implements. `PLACING` is not a reserved
25807                    // token in SPG, so it arrives as a bare Ident.
25808                    if first.eq_ignore_ascii_case("overlay")
25809                        && args.len() == 1
25810                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25811                    {
25812                        self.advance(); // consume PLACING
25813                        args.push(self.parse_expr(0)?); // replacement
25814                        if !matches!(self.peek(), Token::From) {
25815                            return Err(self.err(format!(
25816                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25817                                self.peek()
25818                            )));
25819                        }
25820                        self.advance();
25821                        args.push(self.parse_expr(0)?); // start position
25822                        if matches!(self.peek(), Token::For) {
25823                            self.advance();
25824                            args.push(self.parse_expr(0)?); // length
25825                        }
25826                        if !matches!(self.peek(), Token::RParen) {
25827                            return Err(self.err(format!(
25828                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25829                                self.peek()
25830                            )));
25831                        }
25832                        self.advance();
25833                        return Ok(Expr::FunctionCall {
25834                            name: String::from("overlay"),
25835                            args,
25836                        });
25837                    }
25838                    // `TRIM(chars FROM str)` — the keyword-less
25839                    // spelling lands here after the chars parse
25840                    // (the keyword forms return earlier).
25841                    if first.eq_ignore_ascii_case("trim")
25842                        && args.len() == 1
25843                        && matches!(self.peek(), Token::From)
25844                    {
25845                        self.advance();
25846                        let target = self.parse_expr(0)?;
25847                        if !matches!(self.peek(), Token::RParen) {
25848                            return Err(self.err(format!(
25849                                "expected ')' to close TRIM(chars FROM str), got {:?}",
25850                                self.peek()
25851                            )));
25852                        }
25853                        self.advance();
25854                        let chars = args.pop().expect("one arg");
25855                        return Ok(Expr::FunctionCall {
25856                            name: String::from("btrim"),
25857                            args: alloc::vec![target, chars],
25858                        });
25859                    }
25860                    // v7.24 (round-16 A) — aggregate-internal
25861                    // ordering: `array_agg(x ORDER BY y DESC NULLS
25862                    // LAST)`. Keys close the argument list.
25863                    if matches!(self.peek(), Token::Order) {
25864                        self.advance();
25865                        if !self.peek_is_by() {
25866                            return Err(self.err(format!(
25867                                "expected BY after ORDER in aggregate args, got {:?}",
25868                                self.peek()
25869                            )));
25870                        }
25871                        self.advance();
25872                        loop {
25873                            // v7.39 (round 691) — save/restore, the discipline this parser
25874                            // already uses around `pending_sample_preds`, so a subquery inside
25875                            // a key neither inherits nor leaks the channel.
25876                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25877                            let saved_coll = self.order_key_collation.take();
25878                            let parsed = self.parse_expr(0);
25879                            self.in_order_by_key = saved_flag;
25880                            let collation =
25881                                core::mem::replace(&mut self.order_key_collation, saved_coll);
25882                            let expr = parsed?;
25883                            let desc = if matches!(self.peek(), Token::Desc) {
25884                                self.advance();
25885                                true
25886                            } else if matches!(self.peek(), Token::Asc) {
25887                                self.advance();
25888                                false
25889                            } else {
25890                                false
25891                            };
25892                            let nulls_first = self.parse_optional_nulls_placement()?;
25893                            agg_order_by.push(OrderBy {
25894                                expr,
25895                                desc,
25896                                nulls_first,
25897                                collation,
25898                            });
25899                            if matches!(self.peek(), Token::Comma) {
25900                                self.advance();
25901                            } else {
25902                                break;
25903                            }
25904                        }
25905                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25906                        // follow the ORDER BY inside GROUP_CONCAT.
25907                        if self.consume_group_concat_separator(&mut args)? {
25908                            saw_separator = true;
25909                        }
25910                        if !matches!(self.peek(), Token::RParen) {
25911                            return Err(self.err(format!(
25912                                "expected ')' after aggregate ORDER BY, got {:?}",
25913                                self.peek()
25914                            )));
25915                        }
25916                        break;
25917                    }
25918                    // v7.39 (round 354, M12) — …or directly after the
25919                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25920                    // own spelling of what PG passes as string_agg's second
25921                    // argument; it was a parse error, so every MySQL query
25922                    // that names its own separator failed outright.
25923                    if self.consume_group_concat_separator(&mut args)? {
25924                        saw_separator = true;
25925                        break;
25926                    }
25927                    match self.peek() {
25928                        Token::Comma => {
25929                            self.advance();
25930                        }
25931                        Token::RParen => break,
25932                        other => {
25933                            return Err(self.err(format!(
25934                                "expected ',' or ')' in function args, got {other:?}"
25935                            )));
25936                        }
25937                    }
25938                }
25939            }
25940            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25941            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25942            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25943            // meaning a separator — that is what the explicit SEPARATOR
25944            // tail is for. Fold them into one `concat(...)` so the
25945            // aggregate keeps its single value argument.
25946            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25947                let values = args.len() - usize::from(saw_separator);
25948                if values > 1 {
25949                    let sep_arg = if saw_separator { args.pop() } else { None };
25950                    let folded = Expr::FunctionCall {
25951                        name: "concat".to_string(),
25952                        args: core::mem::take(&mut args),
25953                    };
25954                    args.push(folded);
25955                    if let Some(sep) = sep_arg {
25956                        args.push(sep);
25957                    }
25958                }
25959            }
25960            self.advance(); // consume ')'
25961            // v7.39 (read01 round 77) — named arguments are NOT reordered here
25962            // any more. The parser has no catalog, so it could only ever resolve
25963            // the handful of `make_*` builtins whose parameter names were baked
25964            // into a table right here — every user function got
25965            // "does not support named arguments", though the catalog has been
25966            // storing its parameter names all along. Reordering happens in eval,
25967            // in one place, for builtins and user functions alike.
25968            // v7.32 (round-29) — ordered-set aggregate tail
25969            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25970            // (percentile_cont / percentile_disc / mode). The sort spec
25971            // lands in the same `order_by` slot a decorated aggregate
25972            // uses; the executor dispatches on the function name. WITHIN
25973            // GROUP and an intra-argument ORDER BY are mutually
25974            // exclusive (PG rejects both).
25975            let within_group_order = self.parse_within_group_clause()?;
25976            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25977                return Err(self.err(
25978                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25979                        .into(),
25980                ));
25981            }
25982            let within_group_seen = !within_group_order.is_empty();
25983            let agg_order_by = if within_group_order.is_empty() {
25984                agg_order_by
25985            } else {
25986                within_group_order
25987            };
25988            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25989            let filter = self.parse_filter_clause()?;
25990            // v4.12: window-function tail — `name(args) OVER (...)`.
25991            // Promotes the just-parsed FunctionCall into a
25992            // WindowFunction node carrying partition + order.
25993            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25994            // / `RESPECT NULLS OVER (...)` between the closing paren
25995            // and `OVER`.
25996            let null_treatment = self.parse_null_treatment_modifier();
25997            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25998                && s.eq_ignore_ascii_case("over")
25999            {
26000                self.advance();
26001                // v7.39 (round 230) — PG implements neither modifier for a
26002                // windowed call and says so (0A000). Both used to be parsed
26003                // and then silently dropped here, so `count(DISTINCT v)
26004                // OVER (…)` quietly answered the non-distinct count.
26005                if agg_distinct {
26006                    return Err(
26007                        self.err("DISTINCT is not implemented for window functions".to_string())
26008                    );
26009                }
26010                if !agg_order_by.is_empty() {
26011                    // PG separates the two shapes that land here: a
26012                    // WITHIN GROUP call is an ordered-set aggregate and gets
26013                    // its own message naming the aggregate; a plain
26014                    // `agg(x ORDER BY y)` gets the generic one.
26015                    let msg = if within_group_seen {
26016                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
26017                    } else {
26018                        "aggregate ORDER BY is not implemented for window functions".to_string()
26019                    };
26020                    return Err(self.err(msg));
26021                }
26022                let (partition_by, order_by, frame) = self.parse_over_clause()?;
26023                return Ok(Expr::WindowFunction {
26024                    name: first,
26025                    args,
26026                    partition_by,
26027                    order_by,
26028                    frame,
26029                    null_treatment,
26030                    filter,
26031                });
26032            }
26033            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
26034                return Ok(Expr::AggregateOrdered {
26035                    call: Box::new(Expr::FunctionCall { name: first, args }),
26036                    order_by: agg_order_by,
26037                    distinct: agg_distinct,
26038                    filter,
26039                });
26040            }
26041            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
26042            // over TIMESTAMPTZ and has no timestamp overload, so a
26043            // timestamp argument is coerced on the way in and the answer
26044            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
26045            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
26046            // zone`. SPG answered `timestamp without time zone`, dropping
26047            // the offset from every rendering.
26048            //
26049            // Writing the coercion PG performs makes the existing
26050            // argument-driven typing (the one `date_trunc` uses) reach the
26051            // right answer, rather than teaching the type layer a second
26052            // rule. MySQL's DATE_ADD is a different function that returns
26053            // DATE or DATETIME, so this is PG-dialect only.
26054            //
26055            // Out-of-line because this sits on the RECURSIVE descent
26056            // frame: an inline block with locals here costs every nesting
26057            // level, and the suite's deep-nesting sentinel overflowed the
26058            // 512 KiB parser stack the moment one was added (round 430's
26059            // lesson, in the same shape).
26060            if !self.mysql_dialect {
26061                lift_date_add_arg_to_timestamptz(&first, &mut args);
26062            }
26063            return Ok(Expr::FunctionCall { name: first, args });
26064        }
26065        // v7.9.20 — SQL-standard parenless keyword expressions
26066        // (PG treats these as functions called without parens).
26067        // Resolve to a synthetic FunctionCall so the engine's
26068        // eval path reuses the existing function-call routing.
26069        // mailrs G3.
26070        let lc = first.to_ascii_lowercase();
26071        if matches!(
26072            lc.as_str(),
26073            "current_date"
26074                | "current_time"
26075                | "current_timestamp"
26076                | "localtimestamp"
26077                | "localtime"
26078                // v7.37.17 (17.6 siblings) — session-identity SQL-
26079                // standard parenless keywords. current_user /
26080                // session_user / user were already caught by the
26081                // pgwire canned-response shortcut but bare-select
26082                // in the embedded engine went through Expr::Column
26083                // and errored. Adding them here so the parser
26084                // resolves to a synthetic FunctionCall that reuses
26085                // the existing eval/functions.rs dispatch.
26086                | "current_user"
26087                | "session_user"
26088                | "current_role"
26089                | "current_catalog"
26090                | "current_schema"
26091                | "current_database"
26092                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
26093                | "system_user"
26094        ) {
26095            return Ok(Expr::FunctionCall {
26096                name: lc,
26097                args: Vec::new(),
26098            });
26099        }
26100        Ok(Expr::Column(ColumnName {
26101            qualifier: None,
26102            name: first,
26103        }))
26104    }
26105}
26106
26107/// v7.39 (round 522) — write the coercion PG's `date_add` /
26108/// `date_subtract` signature performs.
26109///
26110/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
26111/// timestamp argument is cast on the way in and the answer is
26112/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
26113/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
26114/// `timestamp without time zone`, dropping the offset from every
26115/// rendering of the result.
26116///
26117/// Writing the cast the signature implies lets the existing
26118/// argument-driven typing (the one `date_trunc` uses) reach the right
26119/// answer instead of teaching the type layer a second rule. MySQL's
26120/// DATE_ADD is a different function returning DATE or DATETIME, so the
26121/// caller applies this in PG dialect only.
26122///
26123/// A free function, and not a block at the call site, because the caller
26124/// is on the recursive-descent frame chain.
26125#[inline(never)]
26126fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
26127    if args.len() != 2
26128        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
26129    {
26130        return;
26131    }
26132    let base = args.remove(0);
26133    args.insert(
26134        0,
26135        Expr::Cast {
26136            expr: Box::new(base),
26137            target: CastTarget::Timestamptz,
26138        },
26139    );
26140}
26141
26142/// v6.8.2 — walk an expression tree and return the first column
26143/// reference's bare name. Used by `parse_create_index_stmt_after_create`
26144/// to derive `CreateIndexStatement.column` from an expression
26145/// key (so downstream planner code resolving a primary column
26146/// position keeps working with expression indexes). Returns
26147/// `None` when the expression has no column ref at all — caller
26148/// surfaces that as a parse error.
26149fn extract_first_column(expr: &Expr) -> Option<String> {
26150    match expr {
26151        Expr::Column(cn) => Some(cn.name.clone()),
26152        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
26153        Expr::Binary { lhs, rhs, .. } => {
26154            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
26155        }
26156        Expr::Unary { expr: e, .. } => extract_first_column(e),
26157        // v7.39 (read01 round 93) — a cast wraps its operand: a common
26158        // expression-index key is `lower(col::text)`, where the column
26159        // sits under the `::text` cast inside the function arg. Without
26160        // descending here the key was rejected as "references no column".
26161        Expr::Cast { expr: e, .. } => extract_first_column(e),
26162        _ => None,
26163    }
26164}
26165
26166fn maybe_not(expr: Expr, negated: bool) -> Expr {
26167    if negated {
26168        Expr::Unary {
26169            op: UnOp::Not,
26170            expr: Box::new(expr),
26171        }
26172    } else {
26173        expr
26174    }
26175}
26176
26177/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
26178/// things in the two dialects, and SPG read all three PG's way:
26179///
26180/// | token | PG (and SPG) | MySQL, measured |
26181/// |---|---|---|
26182/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
26183/// | `&&` | inet / array overlap | **AND** |
26184/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
26185///
26186/// `1 || 0` answering the string '10' on a MySQL session is a wrong
26187/// answer with no error, which is why they are routed here rather than
26188/// left to the shared table.
26189impl Parser {
26190    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
26191        if self.mysql_dialect {
26192            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
26193            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
26194            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
26195            if let Token::Ident(w) = tok
26196                && w.eq_ignore_ascii_case("div")
26197            {
26198                return Some((BinOp::IntDiv, 8));
26199            }
26200            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
26201            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
26202            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
26203            // there sits in operand position, not infix).
26204            if let Token::Ident(w) = tok
26205                && w.eq_ignore_ascii_case("mod")
26206            {
26207                return Some((BinOp::Mod, 8));
26208            }
26209            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
26210            // plain ident to the lexer. Its precedence sits between OR (1)
26211            // and AND (3) — hence rung 2, the slot freed by moving AND up.
26212            if let Token::Ident(w) = tok
26213                && w.eq_ignore_ascii_case("xor")
26214            {
26215                return Some((BinOp::LogicalXor, 2));
26216            }
26217            match tok {
26218                Token::Concat => return Some((BinOp::Or, 1)),
26219                // MySQL's `&&` is logical AND, sharing AND's rung (3).
26220                Token::InetOverlap => return Some((BinOp::And, 3)),
26221                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
26222                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
26223                _ => {}
26224            }
26225        }
26226        binop_from(tok)
26227    }
26228}
26229
26230// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
26231// (which sits strictly between OR and AND), every level from AND upward was
26232// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
26233// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
26234// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
26235// the *relative* order of every PG operator is unchanged by the shift.
26236fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
26237    let pair = match tok {
26238        Token::Or => (BinOp::Or, 1),
26239        Token::And => (BinOp::And, 3),
26240        Token::Eq => (BinOp::Eq, 5),
26241        Token::NotEq => (BinOp::NotEq, 5),
26242        Token::Lt => (BinOp::Lt, 5),
26243        Token::LtEq => (BinOp::LtEq, 5),
26244        Token::Gt => (BinOp::Gt, 5),
26245        Token::GtEq => (BinOp::GtEq, 5),
26246        // pgvector distance ops all sit on the same rung — tighter than
26247        // comparisons (5) so `col <-> v < threshold` parses correctly.
26248        Token::L2Distance => (BinOp::L2Distance, 6),
26249        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
26250        // comparison rung.
26251        Token::GeomParallel => (BinOp::GeomParallel, 5),
26252        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
26253        // comparison rung.
26254        Token::OverLeft => (BinOp::OverLeft, 5),
26255        Token::OverRight => (BinOp::OverRight, 5),
26256        Token::GeomPerp => (BinOp::GeomPerp, 5),
26257        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
26258        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
26259        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
26260        Token::InnerProduct => (BinOp::InnerProduct, 6),
26261        Token::CosineDistance => (BinOp::CosineDistance, 6),
26262        Token::Plus => (BinOp::Add, 7),
26263        Token::Minus => (BinOp::Sub, 7),
26264        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
26265        // binds every "other" operator (`||`, `|`, `&`, `#`, the
26266        // pgvector distances above) BETWEEN additive (7) and the
26267        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
26268        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
26269        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
26270        // ("matches PG conceptually" — the round-753 audit measured it
26271        // false; the old rung errored on `'a' || 1 + 1` with
26272        // `text + integer`). Same-level chains left-fold, as PG does.
26273        Token::Concat => (BinOp::Concat, 6),
26274        Token::Pipe => (BinOp::BitOr, 6),
26275        Token::Amp => (BinOp::BitAnd, 6),
26276        Token::Star => (BinOp::Mul, 8),
26277        Token::Slash => (BinOp::Div, 8),
26278        Token::Percent => (BinOp::Mod, 8),
26279        // v4.14: JSON path ops bind tighter than comparisons (5)
26280        // and additive (7) so `doc->'k' = 'v'` parses correctly.
26281        // Same rung as the multiplicative ops.
26282        Token::JsonGet => (BinOp::JsonGet, 8),
26283        Token::JsonGetText => (BinOp::JsonGetText, 8),
26284        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26285        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26286        Token::JsonContains => (BinOp::JsonContains, 8),
26287        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26288        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26289        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26290        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26291        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26292        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26293        // v7.12.2 — `@@` binds at the comparison rung (looser than
26294        // arithmetic, tighter than AND / OR). PG places `@@` at
26295        // the same precedence as `=` / `<`, so we follow.
26296        Token::TsMatch => (BinOp::TsMatch, 5),
26297        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26298        // PG places these at the comparison rung (same level as `=`),
26299        // so we follow.
26300        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26301        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26302        Token::InetContains => (BinOp::InetContains, 5),
26303        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26304        Token::InetOverlap => (BinOp::InetOverlap, 5),
26305        // v7.39 (round 508) — the geometric and pattern-order predicates
26306        // ride the comparison rung, as every other predicate does.
26307        Token::Intersects => (BinOp::Intersects, 5),
26308        Token::IsBelow => (BinOp::IsBelow, 5),
26309        Token::IsAbove => (BinOp::IsAbove, 5),
26310        Token::PatternLt => (BinOp::PatternLt, 5),
26311        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26312        Token::PatternGt => (BinOp::PatternGt, 5),
26313        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26314        // `@@@` is the old spelling of `@@` and means exactly it.
26315        Token::TsMatchOld => (BinOp::TsMatch, 5),
26316        _ => return None,
26317    };
26318    Some(pair)
26319}
26320
26321#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26322// `as f32` here is intentional: vector elements widen / narrow into f32 on
26323// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26324// past ~15 decimal digits — both are acceptable for a fixed-precision
26325// pgvector column.
26326/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26327/// implicit table alias and break trailing clauses. WITH lands
26328/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26329/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26330/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26331/// / VALUES / FOR / LATERAL — all of which would otherwise be
26332/// silently swallowed by `parse_optional_alias`.
26333fn is_alias_stopword(s: &str) -> bool {
26334    matches!(
26335        s.to_ascii_lowercase().as_str(),
26336        "with"
26337            | "on"
26338            | "where"
26339            | "having"
26340            | "group"
26341            | "order"
26342            | "limit"
26343            | "offset"
26344            | "union"
26345            | "except"
26346            | "intersect"
26347            | "returning"
26348            | "set"
26349            | "values"
26350            | "for"
26351            | "window"
26352            | "tablesample"
26353            | "lateral"
26354            | "left"
26355            | "right"
26356            | "inner"
26357            | "outer"
26358            | "full"
26359            | "cross"
26360            | "join"
26361            | "natural"
26362            | "using"
26363            | "fetch"
26364    )
26365}
26366
26367fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26368    match e {
26369        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26370        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26371        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26372        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26373        // so scale the divisor by hand instead of `f32::powi`.)
26374        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26375            let mut div = 1.0f32;
26376            for _ in 0..*scale {
26377                div *= 10.0;
26378            }
26379            Some(*unscaled as f32 / div)
26380        }
26381        Expr::Unary {
26382            op: UnOp::Neg,
26383            expr,
26384        } => extract_numeric_literal(expr).map(|x| -x),
26385        _ => None,
26386    }
26387}
26388
26389/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26390/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26391/// negative. Returns `None` if any pair fails to parse or no pair is found.
26392///
26393/// Recognised units (case-insensitive, optional trailing `s`):
26394/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26395/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26396/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26397/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26398/// (PG-canonical: DST and month-boundary semantics depend on this).
26399/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26400/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26401/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26402#[allow(clippy::cast_possible_truncation)]
26403fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26404    let mut months: i64 = 0;
26405    let mut days: i64 = 0;
26406    let mut micros: i64 = 0;
26407    let mut in_time = false;
26408    let mut num = alloc::string::String::new();
26409    for ch in rest.chars() {
26410        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26411            num.push(ch);
26412            continue;
26413        }
26414        if ch == 'T' || ch == 't' {
26415            if !num.is_empty() {
26416                return None;
26417            }
26418            in_time = true;
26419            continue;
26420        }
26421        let n: f64 = num.parse().ok()?;
26422        num.clear();
26423        match (ch, in_time) {
26424            ('Y' | 'y', false) => months += (n * 12.0) as i64,
26425            ('M', false) => months += n as i64,
26426            ('W' | 'w', false) => days += (n * 7.0) as i64,
26427            ('D' | 'd', false) => days += n as i64,
26428            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26429            ('M', true) => micros += (n * 60_000_000.0) as i64,
26430            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26431            _ => return None,
26432        }
26433    }
26434    if !num.is_empty() {
26435        return None;
26436    }
26437    Some((
26438        i32::try_from(months).ok()?,
26439        i32::try_from(days).ok()?,
26440        micros,
26441    ))
26442}
26443
26444/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26445/// leading `-` negates the whole value). Rejects date-like strings.
26446fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26447    let (neg, body) = match s.strip_prefix('-') {
26448        Some(b) => (true, b),
26449        None => (false, s),
26450    };
26451    let (y, m) = body.split_once('-')?;
26452    let years: i32 = y.parse().ok()?;
26453    let mons: i32 = m.parse().ok()?;
26454    if years < 0 || mons < 0 {
26455        return None;
26456    }
26457    let total = years.checked_mul(12)?.checked_add(mons)?;
26458    Some((if neg { -total } else { total }, 0, 0))
26459}
26460
26461/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26462/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26463fn parse_interval_clock(tok: &str) -> Option<i64> {
26464    let (neg, body) = match tok.strip_prefix('-') {
26465        Some(r) => (true, r),
26466        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26467    };
26468    let mut it = body.split(':');
26469    let h: i64 = it.next()?.parse().ok()?;
26470    let m: i64 = it.next()?.parse().ok()?;
26471    let s_tok = it.next().unwrap_or("0");
26472    if it.next().is_some() {
26473        return None;
26474    }
26475    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26476        let sec: i64 = sec.parse().ok()?;
26477        let mut f = alloc::string::String::from(frac);
26478        while f.len() < 6 {
26479            f.push('0');
26480        }
26481        f.truncate(6);
26482        let fus: i64 = f.parse().ok()?;
26483        sec.checked_mul(1_000_000)?.checked_add(fus)?
26484    } else {
26485        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26486    };
26487    let total = h
26488        .checked_mul(3_600_000_000)?
26489        .checked_add(m.checked_mul(60_000_000)?)?
26490        .checked_add(sec_us)?;
26491    Some(if neg { -total } else { total })
26492}
26493
26494/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26495/// every spelling PG accepts (measured against live PG18.4, not guessed):
26496/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26497/// Before this, the unit table matched long names only, with an ad-hoc
26498/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26499/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26500/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26501/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26502/// fractional) both read from this one table now.
26503fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26504    let u = raw.to_ascii_lowercase();
26505    Some(match u.as_str() {
26506        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26507            "microsecond"
26508        }
26509        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26510            "millisecond"
26511        }
26512        "second" | "seconds" | "sec" | "secs" | "s" => "second",
26513        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26514        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26515        "day" | "days" | "d" => "day",
26516        "week" | "weeks" | "w" => "week",
26517        "month" | "months" | "mon" | "mons" => "month",
26518        "year" | "years" | "yr" | "yrs" | "y" => "year",
26519        "decade" | "decades" | "dec" | "decs" => "decade",
26520        "century" | "centuries" | "cent" | "c" => "century",
26521        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26522        _ => return None,
26523    })
26524}
26525
26526/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26527/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26529pub(crate) enum IntervalField {
26530    Year,
26531    Month,
26532    Day,
26533    Hour,
26534    Minute,
26535    Second,
26536}
26537
26538/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26539/// spellings aren't standard for the qualifier position, so only the singular
26540/// forms are accepted.
26541/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26542/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26543/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26544/// take a `'1 2'` style literal — are not read here; they stay a parse
26545/// error rather than being silently misread.)
26546/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26547///
26548/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26549/// to do with a `@@` engine setting, and an unset one reads NULL rather
26550/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26551/// were the same node and `SELECT @x` answered "Unknown system variable".)
26552/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26553/// not see a session override — measured, after `SET autocommit=0`,
26554/// `@@global.autocommit` is still 1.
26555///
26556/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26557/// the parser's nesting budget is tuned against, and building these
26558/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26559/// wall `parse_left_right_atom` and friends were factored out for).
26560#[inline(never)]
26561fn variable_ref_atom(raw: &str) -> Expr {
26562    let user_var = !raw.starts_with("@@");
26563    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26564    Expr::FunctionCall {
26565        name: String::from(if user_var {
26566            "__spg_user_var"
26567        } else {
26568            "__spg_session_var"
26569        }),
26570        args: alloc::vec![Expr::Literal(Literal::String(bare))],
26571    }
26572}
26573
26574fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26575    let Token::Ident(s) = tok else { return None };
26576    Some(match () {
26577        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26578        () if s.eq_ignore_ascii_case("second") => "second",
26579        () if s.eq_ignore_ascii_case("minute") => "minute",
26580        () if s.eq_ignore_ascii_case("hour") => "hour",
26581        () if s.eq_ignore_ascii_case("day") => "day",
26582        () if s.eq_ignore_ascii_case("week") => "week",
26583        () if s.eq_ignore_ascii_case("month") => "month",
26584        () if s.eq_ignore_ascii_case("quarter") => "quarter",
26585        () if s.eq_ignore_ascii_case("year") => "year",
26586        () => return None,
26587    })
26588}
26589
26590/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26591/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26592/// which constructs the value at run time. Only the slot the unit names
26593/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26594/// slot the builtin has (months and fractional seconds respectively).
26595fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26596    let zero = || Expr::Literal(Literal::Integer(0));
26597    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26598        lhs: alloc::boxed::Box::new(qty.clone()),
26599        op,
26600        rhs: alloc::boxed::Box::new(by),
26601    };
26602    // (years, months, weeks, days, hours, mins, secs)
26603    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26604    match unit {
26605        "year" => args[0] = qty,
26606        "quarter" => {
26607            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26608        }
26609        "month" => args[1] = qty,
26610        "week" => args[2] = qty,
26611        "day" => args[3] = qty,
26612        "hour" => args[4] = qty,
26613        "minute" => args[5] = qty,
26614        "second" => args[6] = qty,
26615        // The builtin's seconds slot takes a fraction, so microseconds ride
26616        // it scaled down; the divisor is a NUMERIC literal so the division
26617        // stays exact rather than going through a float.
26618        "microsecond" => {
26619            args[6] = scaled(
26620                crate::ast::BinOp::Div,
26621                Expr::Literal(Literal::Numeric {
26622                    unscaled: 1_000_000,
26623                    scale: 0,
26624                }),
26625            );
26626        }
26627        _ => args[3] = qty,
26628    }
26629    Expr::FunctionCall {
26630        name: alloc::string::String::from("make_interval"),
26631        args,
26632    }
26633}
26634
26635/// `(count, unit)` → `(months, days, micros)`.
26636fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26637    let n: i64 = count.trim().parse().ok()?;
26638    Some(match unit {
26639        "microsecond" => (0, 0, n),
26640        "second" => (0, 0, n.checked_mul(1_000_000)?),
26641        "minute" => (0, 0, n.checked_mul(60_000_000)?),
26642        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26643        "day" => (0, i32::try_from(n).ok()?, 0),
26644        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26645        "month" => (i32::try_from(n).ok()?, 0, 0),
26646        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26647        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26648        _ => return None,
26649    })
26650}
26651
26652fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26653    let Token::Ident(s) = tok else { return None };
26654    Some(match () {
26655        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26656        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26657        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26658        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26659        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26660        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26661        () => return None,
26662    })
26663}
26664
26665/// v7.39 (read01 round 102) — interpret an interval literal under a field
26666/// qualifier. Returns `(months, days, micros)`.
26667///
26668/// * A single field applied to a bare number sets which unit the number means,
26669///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26670///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26671/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26672/// * Every other range, and any literal a single field can't read as a plain
26673///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26674///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26675///   like PG, and the qualifier there only bounds precision.
26676fn interpret_qualified_interval(
26677    text: &str,
26678    (f1, f2): (IntervalField, Option<IntervalField>),
26679) -> Option<(i32, i32, i64)> {
26680    if let Some(f2) = f2 {
26681        if f1 == IntervalField::Year && f2 == IntervalField::Month {
26682            if let Some(m) = parse_year_month_literal(text) {
26683                return Some((m, 0, 0));
26684            }
26685        }
26686        return parse_interval_text(text);
26687    }
26688    // Single field: reinterpret a bare number; otherwise the default parse.
26689    let trimmed = text.trim();
26690    if let Ok(val) = trimmed.parse::<f64>() {
26691        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26692        #[allow(clippy::cast_possible_truncation)]
26693        let whole = val as i64;
26694        #[allow(clippy::cast_possible_truncation)]
26695        let secs_micros = {
26696            let m = val * 1_000_000.0;
26697            if m >= 0.0 {
26698                (m + 0.5) as i64
26699            } else {
26700                (m - 0.5) as i64
26701            }
26702        };
26703        return Some(match f1 {
26704            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26705            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26706            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26707            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26708            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26709            IntervalField::Second => (0, 0, secs_micros),
26710        });
26711    }
26712    parse_interval_text(text)
26713}
26714
26715/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26716fn parse_year_month_literal(text: &str) -> Option<i32> {
26717    let t = text.trim();
26718    let (neg, body) = match t.strip_prefix('-') {
26719        Some(r) => (true, r),
26720        None => (false, t.strip_prefix('+').unwrap_or(t)),
26721    };
26722    let mut it = body.split('-');
26723    let years: i32 = it.next()?.trim().parse().ok()?;
26724    let months: i32 = match it.next() {
26725        Some(m) => m.trim().parse().ok()?,
26726        None => 0,
26727    };
26728    if it.next().is_some() {
26729        return None;
26730    }
26731    let total = years.checked_mul(12)?.checked_add(months)?;
26732    Some(if neg { -total } else { total })
26733}
26734
26735pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26736    // v7.38.19 — the two infinities, answered as the three extreme
26737    // fields PostgreSQL itself puts on the wire for them:
26738    //
26739    //   COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
26740    //     … 7fffffffffffffff 7fffffff 7fffffff
26741    //
26742    // So no caller has to know the spelling — every one of them already
26743    // reads the three numbers, and `IntervalKind::from_fields` names
26744    // what they mean.
26745    //
26746    // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
26747    // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
26748    // infinity. Interval takes the full word, in any case.
26749    {
26750        let word = s.trim();
26751        let word = word.strip_prefix('@').map_or(word, str::trim);
26752        let (neg, body) = match word.strip_prefix('-') {
26753            Some(rest) => (true, rest.trim_start()),
26754            None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
26755        };
26756        if body.eq_ignore_ascii_case("infinity") {
26757            return Some(if neg {
26758                (i32::MIN, i32::MIN, i64::MIN)
26759            } else {
26760                (i32::MAX, i32::MAX, i64::MAX)
26761            });
26762        }
26763    }
26764    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26765    // `@` is decorative; a trailing `ago` negates the whole interval.
26766    let mut trimmed = s.trim();
26767    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26768    let mut negate = false;
26769    if let Some(rest) = trimmed
26770        .strip_suffix("ago")
26771        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26772    {
26773        negate = true;
26774        trimmed = rest.trim();
26775    }
26776    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26777        let (mo, d, us) = v?;
26778        if negate {
26779            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26780        } else {
26781            Some((mo, d, us))
26782        }
26783    };
26784    let s = trimmed;
26785    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26786    // are single tokens, not the `<n> <unit>` pair form handled below.
26787    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26788        return finish(parse_iso8601_interval(rest));
26789    }
26790    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26791        if let Some(iv) = parse_year_month_interval(trimmed) {
26792            return finish(Some(iv));
26793        }
26794    }
26795    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26796    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26797    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26798    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26799        if let Ok(n) = trimmed.parse::<i64>() {
26800            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26801        }
26802        if let Ok(f) = trimmed.parse::<f64>() {
26803            if f.is_finite() {
26804                #[allow(clippy::cast_possible_truncation)]
26805                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26806            }
26807        }
26808    }
26809    // v7.39 (round 243) — PG accepts the number and unit run together
26810    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26811    // the `<n> <unit>` pair loop below sees them as two.
26812    let raw_parts: Vec<&str> = s.split_whitespace().collect();
26813    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26814    for p in raw_parts {
26815        let boundary = p
26816            .char_indices()
26817            .find(|(i, c)| {
26818                *i > 0
26819                    && c.is_ascii_alphabetic()
26820                    && p[..*i]
26821                        .chars()
26822                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26823                    && p[..*i].chars().any(|d| d.is_ascii_digit())
26824            })
26825            .map(|(i, _)| i);
26826        match boundary {
26827            Some(i) => {
26828                parts.push(&p[..i]);
26829                parts.push(&p[i..]);
26830            }
26831            None => parts.push(p),
26832        }
26833    }
26834    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26835    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26836    // remains is the `<n> <unit>` pair form handled below.
26837    let mut clock_us: i64 = 0;
26838    let mut had_clock = false;
26839    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26840        clock_us = parse_interval_clock(parts[pos])?;
26841        parts.remove(pos);
26842        had_clock = true;
26843    }
26844    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26845    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26846    let mut lone_days: i32 = 0;
26847    if had_clock && parts.len() == 1 {
26848        if let Ok(n) = parts[0].parse::<i64>() {
26849            lone_days = i32::try_from(n).ok()?;
26850            parts.clear();
26851        }
26852    }
26853    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26854        return None;
26855    }
26856    let mut months: i32 = 0;
26857    let mut days: i32 = lone_days;
26858    let mut micros: i64 = clock_us;
26859    let mut i = 0;
26860    while i < parts.len() {
26861        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26862        if let Ok(n) = parts[i].parse::<i64>() {
26863            match unit_stripped {
26864                "microsecond" => micros = micros.checked_add(n)?,
26865                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26866                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26867                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26868                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26869                "day" => {
26870                    let n32 = i32::try_from(n).ok()?;
26871                    days = days.checked_add(n32)?;
26872                }
26873                "week" => {
26874                    let n32 = i32::try_from(n).ok()?;
26875                    days = days.checked_add(n32.checked_mul(7)?)?;
26876                }
26877                "month" => {
26878                    let n32 = i32::try_from(n).ok()?;
26879                    months = months.checked_add(n32)?;
26880                }
26881                "year" => {
26882                    let n32 = i32::try_from(n).ok()?;
26883                    months = months.checked_add(n32.checked_mul(12)?)?;
26884                }
26885                // v7.39 (read01 timestamp.c) — the larger calendar units.
26886                "decade" => {
26887                    let n32 = i32::try_from(n).ok()?;
26888                    months = months.checked_add(n32.checked_mul(120)?)?;
26889                }
26890                "century" => {
26891                    let n32 = i32::try_from(n).ok()?;
26892                    months = months.checked_add(n32.checked_mul(1200)?)?;
26893                }
26894                "millennium" => {
26895                    let n32 = i32::try_from(n).ok()?;
26896                    months = months.checked_add(n32.checked_mul(12000)?)?;
26897                }
26898                _ => return None,
26899            }
26900        } else if let Ok(f) = parts[i].parse::<f64>() {
26901            // Fractional units cascade down to the next-finer field the way
26902            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26903            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26904            // no_std: f64 has no trunc/fract/round methods, so do them with
26905            // casts (toward-zero) + explicit round-half-away-from-zero.
26906            #[allow(clippy::cast_possible_truncation)]
26907            fn round_i64(x: f64) -> i64 {
26908                if x >= 0.0 {
26909                    (x + 0.5) as i64
26910                } else {
26911                    (x - 0.5) as i64
26912                }
26913            }
26914            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26915            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26916                const DAY_US: f64 = 86_400_000_000.0;
26917                let whole = d as i64; // truncates toward zero
26918                let frac = d - whole as f64;
26919                *days = days.checked_add(i32::try_from(whole).ok()?)?;
26920                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26921                Some(())
26922            }
26923            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26924            match unit_stripped {
26925                "microsecond" => micros = micros.checked_add(round_i64(f))?,
26926                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26927                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26928                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26929                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26930                "day" => add_days_frac(&mut days, &mut micros, f)?,
26931                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26932                "month" => {
26933                    let whole = f as i64;
26934                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26935                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26936                }
26937                "year" => {
26938                    let m = f * 12.0;
26939                    let whole = m as i64;
26940                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26941                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26942                }
26943                _ => return None,
26944            }
26945        } else {
26946            return None;
26947        }
26948        i += 2;
26949    }
26950    finish(Some((months, days, micros)))
26951}
26952
26953/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26954/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26955/// `interval` is intentionally absent (handled by its own parser arm).
26956/// Returns `None` for names that aren't sensible as a bare typed literal, so
26957/// the caller falls back to treating the ident as a column reference.
26958fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26959    Some(match ident {
26960        "date" => CastTarget::Date,
26961        "timestamp" | "datetime" => CastTarget::Timestamp,
26962        "timestamptz" => CastTarget::Timestamptz,
26963        "bool" | "boolean" => CastTarget::Bool,
26964        "int" | "integer" | "int4" => CastTarget::Int,
26965        "bigint" | "int8" => CastTarget::BigInt,
26966        "float8" | "double precision" => CastTarget::Float,
26967        "uuid" => CastTarget::Uuid,
26968        "bytea" => CastTarget::Bytea,
26969        "json" => CastTarget::Json,
26970        "jsonb" => CastTarget::Jsonb,
26971        // Types without a dedicated CastTarget variant flow through the
26972        // generic Named path (engine resolves via column_type_to_data_type).
26973        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26974        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26975        | "money" | "bit" | "varbit"
26976        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26977        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26978        // Range / multirange types likewise.
26979        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26980        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26981        | "datemultirange" | "tsmultirange" | "tstzmultirange"
26982        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26983        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26984            CastTarget::Named(alloc::string::String::from(ident))
26985        }
26986        _ => return None,
26987    })
26988}
26989
26990/// v7.12.4 — map a bare type-name identifier (the form that
26991/// appears in a function arg list or RETURNS clause) to a
26992/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26993/// types so the caller can preserve them as
26994/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26995///
26996/// Subset of the full column-type grammar — we deliberately
26997/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26998/// here because function-arg types in v7.12.4 are mostly the
26999/// bare form (`text`, `int`, `bytea`, …).
27000/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
27001/// than being `name TYPE`?
27002///
27003/// The multi-word spellings SQL allows for a bare argument type, each
27004/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
27005///
27006/// NOTE this list also exists in `spg-storage`, which computes the
27007/// signature key from the rendered argument text and has to reach the
27008/// same verdict. The two crates are siblings — neither depends on the
27009/// other — and each already carries its own table of type spellings
27010/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
27011/// there), so this follows the structure rather than inventing new
27012/// duplication. Recorded as V49.
27013pub fn is_multiword_type_phrase(phrase: &str) -> bool {
27014    let t = phrase.trim().to_ascii_lowercase();
27015    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
27016    matches!(
27017        base,
27018        "double precision"
27019            | "character varying"
27020            | "bit varying"
27021            | "timestamp with time zone"
27022            | "timestamp without time zone"
27023            | "time with time zone"
27024            | "time without time zone"
27025            | "national character"
27026            | "national character varying"
27027    )
27028}
27029
27030fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
27031    Some(match ident.to_ascii_lowercase().as_str() {
27032        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
27033        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
27034        "bigint" => ColumnTypeName::BigInt,
27035        "float" | "double" => ColumnTypeName::Float,
27036        // v7.39 (round 269) — real is 32-bit.
27037        "real" | "float4" => ColumnTypeName::Real,
27038        "text" => ColumnTypeName::Text,
27039        "bool" | "boolean" => ColumnTypeName::Bool,
27040        "date" => ColumnTypeName::Date,
27041        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
27042        "timestamptz" => ColumnTypeName::Timestamptz,
27043        "json" => ColumnTypeName::Json,
27044        "jsonb" => ColumnTypeName::Jsonb,
27045        "bytea" | "bytes" => ColumnTypeName::Bytes,
27046        "tsvector" => ColumnTypeName::TsVector,
27047        "tsquery" => ColumnTypeName::TsQuery,
27048        "uuid" => ColumnTypeName::Uuid,
27049        "interval" => ColumnTypeName::Interval,
27050        "time" => ColumnTypeName::Time,
27051        "year" => ColumnTypeName::Year,
27052        "timetz" => ColumnTypeName::TimeTz,
27053        "money" => ColumnTypeName::Money,
27054        _ => return None,
27055    })
27056}
27057
27058/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
27059/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
27060///
27061/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
27062/// / embedded SQL land in v7.12.5+):
27063///
27064/// ```text
27065///   body          := [ws] block [ws]
27066///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
27067///   stmt          := assign | return
27068///   assign        := assign_target := expr
27069///   assign_target := ( NEW | OLD ) . ident | ident
27070///   return        := RETURN ( NEW | OLD | NULL | expr )
27071/// ```
27072///
27073/// `expr` is parsed by recursing into the regular `Parser` — so a
27074/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
27075/// NEW.subject || ' ' || NEW.sender)` body shape works without
27076/// the body parser knowing what `to_tsvector` is.
27077///
27078/// Errors here cause the caller to fall back to
27079/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
27080/// successful, but the executor will refuse to invoke the
27081/// function with an "unparseable body" error.
27082/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
27083/// from the crate root as `spg_sql::parse_function_body`.
27084pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27085    parse_plpgsql_body(body)
27086}
27087
27088fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27089    // Use the regular lexer on the body text. The trailing
27090    // `END;` may or may not have a semicolon; the lexer treats
27091    // both forms identically.
27092    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
27093        message: alloc::format!("plpgsql body lex error: {e}"),
27094        token_pos: 0,
27095    })?;
27096    let mut parser = Parser::new(tokens);
27097    parser.parse_plpgsql_block()
27098}
27099
27100/// v7.39 (GUC) — the textual body of a SET value, for list joining.
27101fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
27102    match v {
27103        crate::ast::SetValue::String(s)
27104        | crate::ast::SetValue::Ident(s)
27105        | crate::ast::SetValue::Number(s) => s.clone(),
27106        crate::ast::SetValue::Default => "DEFAULT".into(),
27107    }
27108}
27109
27110/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
27111/// contains an aggregate call at ITS OWN query level (recursion stops at
27112/// sublink boundaries — a sublink's aggregates belong to the sublink).
27113/// Backs the "aggregate functions are not allowed in a recursive query's
27114/// recursive term" well-formedness check.
27115fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
27116    const AGG_NAMES: &[&str] = &[
27117        "count",
27118        "sum",
27119        "min",
27120        "max",
27121        "avg",
27122        "string_agg",
27123        "array_agg",
27124        "bool_and",
27125        "bool_or",
27126        "every",
27127        "any_value",
27128        "json_agg",
27129        "jsonb_agg",
27130        "json_object_agg",
27131        "jsonb_object_agg",
27132        "bit_and",
27133        "bit_or",
27134        "bit_xor",
27135        "var_pop",
27136        "var_samp",
27137        "variance",
27138        "stddev",
27139        "stddev_pop",
27140        "stddev_samp",
27141        "range_agg",
27142        "range_intersect_agg",
27143        "percentile_cont",
27144        "percentile_disc",
27145        "mode",
27146        "corr",
27147        "covar_pop",
27148        "covar_samp",
27149    ];
27150    match e {
27151        Expr::AggregateOrdered { .. } => true,
27152        Expr::FunctionCall { name, args } => {
27153            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
27154                || args.iter().any(expr_has_toplevel_aggregate)
27155        }
27156        Expr::NamedArg { expr, .. }
27157        | Expr::Variadic(expr)
27158        | Expr::Unary { expr, .. }
27159        | Expr::Cast { expr, .. }
27160        | Expr::IsNull { expr, .. }
27161        | Expr::FieldAccess { base: expr, .. }
27162        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
27163        Expr::Binary { lhs, rhs, .. } => {
27164            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
27165        }
27166        Expr::Like { expr, pattern, .. } => {
27167            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
27168        }
27169        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
27170        Expr::InList { expr, list, .. } => {
27171            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
27172        }
27173        Expr::ArraySubscript { target, index } => {
27174            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
27175        }
27176        Expr::ArraySlice { target, lo, hi } => {
27177            expr_has_toplevel_aggregate(target)
27178                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
27179                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
27180        }
27181        Expr::AnyAll { expr, array, .. } => {
27182            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
27183        }
27184        Expr::Case {
27185            operand,
27186            branches,
27187            else_branch,
27188        } => {
27189            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
27190                || branches
27191                    .iter()
27192                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
27193                || else_branch
27194                    .as_deref()
27195                    .is_some_and(expr_has_toplevel_aggregate)
27196        }
27197        // The outer-level operands of a sublink can aggregate; the sublink's
27198        // own body cannot leak its aggregates up here.
27199        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
27200        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
27201            row.iter().any(expr_has_toplevel_aggregate)
27202        }
27203        _ => false,
27204    }
27205}
27206
27207/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
27208/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
27209/// named table anywhere in its subtree. A plain FROM derived table is NOT a
27210/// sublink and is legal in a recursive term, so it is not walked here.
27211fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
27212    let mut exprs: Vec<&Expr> = Vec::new();
27213    for it in &s.items {
27214        if let crate::ast::SelectItem::Expr { expr, .. } = it {
27215            exprs.push(expr);
27216        }
27217    }
27218    if let Some(w) = &s.where_ {
27219        exprs.push(w);
27220    }
27221    if let Some(h) = &s.having {
27222        exprs.push(h);
27223    }
27224    if let Some(g) = &s.group_by {
27225        exprs.extend(g.iter());
27226    }
27227    if let Some(from) = &s.from {
27228        for j in &from.joins {
27229            if let Some(on) = &j.on {
27230                exprs.push(on);
27231            }
27232        }
27233    }
27234    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
27235}
27236
27237/// Does this expression contain a sublink whose subquery mentions `name`?
27238fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
27239    match e {
27240        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
27241        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
27242        Expr::InSubquery { expr, subquery, .. } => {
27243            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
27244        }
27245        Expr::RowInSubquery { row, subquery, .. } => {
27246            row.iter().any(|x| expr_sublink_mentions(x, name))
27247                || select_mentions_table(subquery, name)
27248        }
27249        Expr::RowCmpSubquery { row, subquery, .. } => {
27250            row.iter().any(|x| expr_sublink_mentions(x, name))
27251                || select_mentions_table(subquery, name)
27252        }
27253        Expr::NamedArg { expr, .. }
27254        | Expr::Variadic(expr)
27255        | Expr::Unary { expr, .. }
27256        | Expr::Cast { expr, .. }
27257        | Expr::IsNull { expr, .. }
27258        | Expr::FieldAccess { base: expr, .. }
27259        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
27260        Expr::Binary { lhs, rhs, .. } => {
27261            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
27262        }
27263        Expr::Like { expr, pattern, .. } => {
27264            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
27265        }
27266        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
27267            args.iter().any(|x| expr_sublink_mentions(x, name))
27268        }
27269        Expr::InList { expr, list, .. } => {
27270            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
27271        }
27272        Expr::ArraySubscript { target, index } => {
27273            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
27274        }
27275        Expr::ArraySlice { target, lo, hi } => {
27276            expr_sublink_mentions(target, name)
27277                || lo
27278                    .as_deref()
27279                    .is_some_and(|x| expr_sublink_mentions(x, name))
27280                || hi
27281                    .as_deref()
27282                    .is_some_and(|x| expr_sublink_mentions(x, name))
27283        }
27284        Expr::AnyAll { expr, array, .. } => {
27285            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
27286        }
27287        Expr::Case {
27288            operand,
27289            branches,
27290            else_branch,
27291        } => {
27292            operand
27293                .as_deref()
27294                .is_some_and(|x| expr_sublink_mentions(x, name))
27295                || branches
27296                    .iter()
27297                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
27298                || else_branch
27299                    .as_deref()
27300                    .is_some_and(|x| expr_sublink_mentions(x, name))
27301        }
27302        _ => false,
27303    }
27304}
27305
27306/// Does this SELECT (in full — FROM tables, derived tables, its own
27307/// sublinks, and union arms) mention the named table?
27308fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27309    if let Some(from) = &s.from {
27310        if from.primary.name.eq_ignore_ascii_case(name) {
27311            return true;
27312        }
27313        if let Some(sub) = &from.primary.lateral_subquery
27314            && select_mentions_table(sub, name)
27315        {
27316            return true;
27317        }
27318        for j in &from.joins {
27319            if j.table.name.eq_ignore_ascii_case(name) {
27320                return true;
27321            }
27322            if let Some(sub) = &j.table.lateral_subquery
27323                && select_mentions_table(sub, name)
27324            {
27325                return true;
27326            }
27327        }
27328    }
27329    if select_has_self_ref_in_sublink(s, name) {
27330        return true;
27331    }
27332    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27333}
27334
27335/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27336/// row count, the way PG evaluates one before applying it.
27337///
27338/// `None` = not a constant (a column, a subquery, a function call).
27339/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27340/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27341/// All wordings were read off live PG 18.4.
27342fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27343    use crate::ast::{BinOp, Expr, Literal, UnOp};
27344    match e {
27345        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27346        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27347            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27348        }
27349        // PG coerces a string by its CONTENT, and fails on the value.
27350        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27351            |_| {
27352                Err(alloc::format!(
27353                    "invalid input syntax for type bigint: \"{t}\""
27354                ))
27355            },
27356            |n| Ok(i128::from(n)),
27357        )),
27358        Expr::Literal(Literal::Bool(_)) => Some(Err(
27359            "argument of {L} must be type bigint, not type boolean".into(),
27360        )),
27361        Expr::Unary {
27362            op: UnOp::Neg,
27363            expr,
27364        } => match fold_limit_constant(expr)? {
27365            Ok(v) => Some(Ok(-v)),
27366            e @ Err(_) => Some(e),
27367        },
27368        Expr::Binary { lhs, op, rhs } => {
27369            let a = match fold_limit_constant(lhs)? {
27370                Ok(v) => v,
27371                e @ Err(_) => return Some(e),
27372            };
27373            let b = match fold_limit_constant(rhs)? {
27374                Ok(v) => v,
27375                e @ Err(_) => return Some(e),
27376            };
27377            let out = match op {
27378                BinOp::Add => a.checked_add(b),
27379                BinOp::Sub => a.checked_sub(b),
27380                BinOp::Mul => a.checked_mul(b),
27381                BinOp::Div if b != 0 => a.checked_div(b),
27382                BinOp::Div => return Some(Err("division by zero".into())),
27383                BinOp::Mod if b != 0 => a.checked_rem(b),
27384                BinOp::Mod => return Some(Err("division by zero".into())),
27385                _ => return None,
27386            };
27387            // PG evaluates the arithmetic in the operand's own type, so an
27388            // int-by-int product that leaves int range fails there — before
27389            // the row count is ever looked at.
27390            match out {
27391                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27392                    Some(Err("integer out of range".into()))
27393                }
27394                Some(v) => Some(Ok(v)),
27395                None => Some(Err("integer out of range".into())),
27396            }
27397        }
27398        _ => None,
27399    }
27400}
27401
27402/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27403/// cast, which is what makes `LIMIT 2.5` keep three rows.
27404fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27405    if scale == 0 {
27406        return unscaled;
27407    }
27408    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27409        return 0;
27410    };
27411    let neg = unscaled < 0;
27412    let mag = unscaled.unsigned_abs() as i128;
27413    let rounded = (mag + div / 2) / div;
27414    if neg { -rounded } else { rounded }
27415}
27416
27417#[cfg(test)]
27418mod tests {
27419    use super::*;
27420    use alloc::string::ToString;
27421
27422    fn parse(s: &str) -> Statement {
27423        parse_statement(s).expect("parse ok")
27424    }
27425
27426    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27427    // `tables`, `partition`, etc. are unreserved keywords per PG's
27428    // `pg_get_keywords()` and MUST be usable as column / table /
27429    // alias names. Pre-T4 every drop-in user whose schema had one
27430    // of these as a column name (sentori events.release, mailrs
27431    // messages.index in some forks) blew the parser up at CREATE
27432    // TABLE time with "expected identifier, got Release". The
27433    // generalisation lives in `unreserved_keyword_text` + the
27434    // `expect_ident_like` and `parse_atom` arms that consult it.
27435    #[test]
27436    fn release_usable_as_column_name_in_create_table() {
27437        let stmt =
27438            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27439        if let Statement::CreateTable(t) = stmt {
27440            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27441            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27442        } else {
27443            panic!("expected CreateTable");
27444        }
27445    }
27446
27447    #[test]
27448    fn release_usable_as_column_ref_in_select_projection() {
27449        // The sentori `0003_partition_events.sql` INSERT-SELECT
27450        // walk references `release` in both column lists; the
27451        // projection-side use exercises `parse_atom`'s relaxed
27452        // identifier set.
27453        parse("SELECT id, release, payload FROM events WHERE id = 1");
27454    }
27455
27456    #[test]
27457    fn release_usable_as_column_ref_in_insert_column_list() {
27458        // INSERT INTO t (id, release, payload) VALUES (…)
27459        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27460    }
27461
27462    #[test]
27463    fn alter_column_drop_not_null_uses_keyword_drop_token() {
27464        // Sentori `0013_audit_tombstone.sql` issues
27465        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27466        // emits Token::Drop (not Ident("drop")); the parser must
27467        // accept both in the ALTER COLUMN sub-dispatch.
27468        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27469    }
27470
27471    #[test]
27472    fn create_index_accepts_parenthesised_expression_key() {
27473        // sentori `0040_events_bundle_idx.sql` shape — JSONB
27474        // expression index. Pre-T4 the parser bailed at the
27475        // inner `(` with "expected column ident or expression,
27476        // got LParen". The Token::LParen arm in CREATE INDEX
27477        // routes through the expression parser instead.
27478        parse(
27479            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27480             ON events ((payload->'bundle'->>'id'))",
27481        );
27482    }
27483
27484    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27485    // surface as parse errors, never stack overflows (embed hosts
27486    // abort on overflow).
27487    /// The nesting budget is a COUNT; what it has to fit inside is a
27488    /// number of BYTES, and only one of those two is stable across
27489    /// compiler versions. Round 847 measured 30,336 bytes per level
27490    /// after a toolchain move, which puts 64 levels at 1.94 MB and
27491    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27492    /// aborted instead of erroring, which is precisely the outcome it
27493    /// exists to rule out.
27494    ///
27495    /// So the budget is metered rather than assumed. The ceiling leaves
27496    /// the depth SPG advertises fitting in a default 2 MiB thread with
27497    /// room to spare, in the debug build, where frames are widest.
27498    #[test]
27499    fn nesting_frame_cost_stays_under_ceiling() {
27500        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27501        // thread keeps a margin for whatever called the parser.
27502        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27503
27504        frame_meter::reset();
27505        let depth = frame_meter::SAMPLE_HI + 8;
27506        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27507        parse(&sql);
27508
27509        let per_level = frame_meter::bytes_per_level();
27510        {
27511            extern crate std;
27512            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27513        }
27514        assert!(
27515            per_level <= CEILING,
27516            "{per_level} bytes per nesting level exceeds {CEILING}; \
27517             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27518             in parse_expr_inner / parse_unary rather than lowering the \
27519             depth or widening the stack.",
27520            per_level * MAX_NEST_DEPTH
27521        );
27522    }
27523
27524    #[test]
27525    fn nesting_budget_errors_cleanly() {
27526        let depth = MAX_NEST_DEPTH + 50;
27527        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27528        let err = parse_statement(&sql).expect_err("must reject");
27529        assert!(err.message.contains("nests deeper"), "{err:?}");
27530        // Within budget still parses.
27531        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27532        parse(&sql);
27533    }
27534
27535    #[test]
27536    fn binary_chain_budget_errors_cleanly() {
27537        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27538        let err = parse_statement(&sql).expect_err("must reject");
27539        assert!(err.message.contains("chained binary"), "{err:?}");
27540        // Within budget still parses (chain depth ≤ budget is safe
27541        // for recursive eval/drop on 2 MiB stacks).
27542        let sql = format!("SELECT 1{}", " + 1".repeat(200));
27543        parse(&sql);
27544    }
27545
27546    #[test]
27547    fn in_list_unaffected_by_chain_budget() {
27548        // Flat InList: 20k elements parse fine and stay flat.
27549        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27550        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27551        let Statement::Select(s) = parse(&sql) else {
27552            panic!("expected select")
27553        };
27554        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27555            panic!("expected flat InList, got {:?}", s.where_)
27556        };
27557        assert_eq!(list.len(), 20_000);
27558        assert!(!negated);
27559    }
27560
27561    fn lit_int(n: i64) -> Expr {
27562        Expr::Literal(Literal::Integer(n))
27563    }
27564
27565    fn col(name: &str) -> Expr {
27566        Expr::Column(ColumnName {
27567            qualifier: None,
27568            name: name.into(),
27569        })
27570    }
27571
27572    #[test]
27573    fn select_single_integer() {
27574        let s = parse("SELECT 1");
27575        let Statement::Select(s) = s else {
27576            panic!("expected SELECT")
27577        };
27578        assert_eq!(s.items.len(), 1);
27579        assert!(s.from.is_none());
27580        assert!(s.where_.is_none());
27581    }
27582
27583    #[test]
27584    fn select_multiple_literal_kinds() {
27585        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27586        let Statement::Select(s) = s else {
27587            panic!("expected SELECT")
27588        };
27589        assert_eq!(s.items.len(), 5);
27590    }
27591
27592    #[test]
27593    fn select_wildcard_from_table() {
27594        let s = parse("SELECT * FROM users");
27595        let Statement::Select(s) = s else {
27596            panic!("expected SELECT")
27597        };
27598        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27599        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27600    }
27601
27602    #[test]
27603    fn select_with_table_alias() {
27604        let s = parse("SELECT * FROM users AS u");
27605        let Statement::Select(s) = s else {
27606            panic!("expected SELECT")
27607        };
27608        let t = &s.from.as_ref().unwrap().primary;
27609        assert_eq!(t.name, "users");
27610        assert_eq!(t.alias.as_deref(), Some("u"));
27611    }
27612
27613    #[test]
27614    fn select_with_where_eq() {
27615        let s = parse("SELECT a FROM t WHERE a = 1");
27616        let Statement::Select(s) = s else {
27617            panic!("expected SELECT")
27618        };
27619        let w = s.where_.unwrap();
27620        assert_eq!(
27621            w,
27622            Expr::Binary {
27623                lhs: Box::new(col("a")),
27624                op: BinOp::Eq,
27625                rhs: Box::new(lit_int(1)),
27626            }
27627        );
27628    }
27629
27630    #[test]
27631    fn arithmetic_precedence() {
27632        let s = parse("SELECT 1 + 2 * 3");
27633        let Statement::Select(s) = s else {
27634            panic!("expected SELECT")
27635        };
27636        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27637            panic!("wildcard?")
27638        };
27639        assert_eq!(
27640            expr,
27641            &Expr::Binary {
27642                lhs: Box::new(lit_int(1)),
27643                op: BinOp::Add,
27644                rhs: Box::new(Expr::Binary {
27645                    lhs: Box::new(lit_int(2)),
27646                    op: BinOp::Mul,
27647                    rhs: Box::new(lit_int(3)),
27648                }),
27649            }
27650        );
27651    }
27652
27653    #[test]
27654    fn parentheses_override_precedence() {
27655        let s = parse("SELECT (1 + 2) * 3");
27656        let Statement::Select(s) = s else {
27657            panic!("expected SELECT")
27658        };
27659        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27660            panic!()
27661        };
27662        assert_eq!(
27663            expr,
27664            &Expr::Binary {
27665                lhs: Box::new(Expr::Binary {
27666                    lhs: Box::new(lit_int(1)),
27667                    op: BinOp::Add,
27668                    rhs: Box::new(lit_int(2)),
27669                }),
27670                op: BinOp::Mul,
27671                rhs: Box::new(lit_int(3)),
27672            }
27673        );
27674    }
27675
27676    #[test]
27677    fn not_binds_below_comparison() {
27678        // `NOT a = 1` should parse as `NOT (a = 1)`.
27679        let s = parse("SELECT NOT a = 1 FROM t");
27680        let Statement::Select(s) = s else {
27681            panic!("expected SELECT")
27682        };
27683        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27684            panic!()
27685        };
27686        assert_eq!(
27687            expr,
27688            &Expr::Unary {
27689                op: UnOp::Not,
27690                expr: Box::new(Expr::Binary {
27691                    lhs: Box::new(col("a")),
27692                    op: BinOp::Eq,
27693                    rhs: Box::new(lit_int(1)),
27694                }),
27695            }
27696        );
27697    }
27698
27699    #[test]
27700    fn unary_minus_binds_above_multiplication() {
27701        // `-a * 2` should be `(-a) * 2`.
27702        let s = parse("SELECT -a * 2 FROM t");
27703        let Statement::Select(s) = s else {
27704            panic!("expected SELECT")
27705        };
27706        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27707            panic!()
27708        };
27709        assert_eq!(
27710            expr,
27711            &Expr::Binary {
27712                lhs: Box::new(Expr::Unary {
27713                    op: UnOp::Neg,
27714                    expr: Box::new(col("a")),
27715                }),
27716                op: BinOp::Mul,
27717                rhs: Box::new(lit_int(2)),
27718            }
27719        );
27720    }
27721
27722    #[test]
27723    fn qualified_column() {
27724        let s = parse("SELECT t.col FROM t");
27725        let Statement::Select(s) = s else {
27726            panic!("expected SELECT")
27727        };
27728        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27729            panic!()
27730        };
27731        assert_eq!(
27732            expr,
27733            &Expr::Column(ColumnName {
27734                qualifier: Some("t".into()),
27735                name: "col".into()
27736            })
27737        );
27738    }
27739
27740    #[test]
27741    fn select_item_alias_with_as() {
27742        let s = parse("SELECT a AS y FROM t");
27743        let Statement::Select(s) = s else {
27744            panic!("expected SELECT")
27745        };
27746        let SelectItem::Expr { alias, .. } = &s.items[0] else {
27747            panic!()
27748        };
27749        assert_eq!(alias.as_deref(), Some("y"));
27750    }
27751
27752    #[test]
27753    fn trailing_semicolon_accepted() {
27754        let s = parse("SELECT 1;");
27755        let Statement::Select(s) = s else {
27756            panic!("expected SELECT")
27757        };
27758        assert_eq!(s.items.len(), 1);
27759    }
27760
27761    #[test]
27762    fn boolean_chain_with_and_or_not() {
27763        // (NOT a) OR (b AND (NOT c))
27764        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27765        let Statement::Select(s) = s else {
27766            panic!("expected SELECT")
27767        };
27768        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27769            panic!()
27770        };
27771        let expected = Expr::Binary {
27772            lhs: Box::new(Expr::Unary {
27773                op: UnOp::Not,
27774                expr: Box::new(col("a")),
27775            }),
27776            op: BinOp::Or,
27777            rhs: Box::new(Expr::Binary {
27778                lhs: Box::new(col("b")),
27779                op: BinOp::And,
27780                rhs: Box::new(Expr::Unary {
27781                    op: UnOp::Not,
27782                    expr: Box::new(col("c")),
27783                }),
27784            }),
27785        };
27786        assert_eq!(expr, &expected);
27787    }
27788
27789    #[test]
27790    fn empty_input_errors() {
27791        // v7.14.0 — pg_dump preambles emit several comment-only
27792        // / blank-line statements that collapse to Statement::
27793        // Empty rather than a parse error. The old "SELECT in
27794        // message" assertion is stale; verify the new contract:
27795        // empty / whitespace / comment-only input parses to
27796        // Statement::Empty.
27797        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27798        assert!(matches!(
27799            parse_statement("  \n\t ").unwrap(),
27800            Statement::Empty
27801        ));
27802        // Sanity: malformed-but-non-empty still errors.
27803        assert!(parse_statement("SELECT FROM WHERE").is_err());
27804    }
27805
27806    #[test]
27807    fn unmatched_paren_errors() {
27808        assert!(parse_statement("SELECT (1 + 2").is_err());
27809    }
27810
27811    #[test]
27812    fn display_round_trip_simple_select() {
27813        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27814        let text = original.to_string();
27815        let again = parse_statement(&text).expect("re-parse");
27816        assert_eq!(original, again);
27817    }
27818
27819    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27820
27821    #[test]
27822    fn create_table_single_column() {
27823        let s = parse("CREATE TABLE foo (a INT)");
27824        let Statement::CreateTable(c) = s else {
27825            panic!("expected CreateTable")
27826        };
27827        assert_eq!(c.name, "foo");
27828        assert_eq!(c.columns.len(), 1);
27829        assert_eq!(c.columns[0].name, "a");
27830        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27831        assert!(c.columns[0].nullable);
27832    }
27833
27834    #[test]
27835    fn create_table_multi_column_with_not_null_mix() {
27836        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27837        let Statement::CreateTable(c) = s else {
27838            panic!()
27839        };
27840        assert_eq!(c.columns.len(), 4);
27841        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27842        assert!(!c.columns[0].nullable);
27843        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27844        assert!(c.columns[1].nullable);
27845        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27846        assert!(!c.columns[2].nullable);
27847        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27848    }
27849
27850    #[test]
27851    fn create_table_bigint_supported() {
27852        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27853        let Statement::CreateTable(c) = s else {
27854            panic!()
27855        };
27856        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27857    }
27858
27859    #[test]
27860    fn create_table_vector_default_is_f32() {
27861        let s = parse("CREATE TABLE t (v VECTOR(128))");
27862        let Statement::CreateTable(c) = s else {
27863            panic!()
27864        };
27865        assert_eq!(
27866            c.columns[0].ty,
27867            ColumnTypeName::Vector {
27868                dim: 128,
27869                encoding: VecEncoding::F32,
27870            },
27871        );
27872    }
27873
27874    #[test]
27875    fn create_table_vector_using_sq8() {
27876        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27877        // Case-insensitive on both `USING` and the encoding name.
27878        for sql in [
27879            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27880            "CREATE TABLE t (v VECTOR(128) using sq8)",
27881        ] {
27882            let s = parse(sql);
27883            let Statement::CreateTable(c) = s else {
27884                panic!()
27885            };
27886            assert_eq!(
27887                c.columns[0].ty,
27888                ColumnTypeName::Vector {
27889                    dim: 128,
27890                    encoding: VecEncoding::Sq8,
27891                },
27892                "{sql}",
27893            );
27894        }
27895    }
27896
27897    #[test]
27898    fn create_table_vector_using_unknown_errors() {
27899        // v7.16.1 — the inline `USING <encoding>` shape on
27900        // CREATE TABLE column defs was withdrawn before
27901        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27902        // (col vector_<metric>_ops)`; the parser now rejects
27903        // USING at column-list position with a clearer
27904        // "expected ',' or ')'" message. Test asserts the
27905        // current rejection, not the old "unknown vector
27906        // encoding" string.
27907        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27908        assert!(
27909            err.message.contains("USING")
27910                || err.message.contains("using")
27911                || err.message.contains("')'")
27912                || err.message.contains("','"),
27913            "expected USING/column-list rejection, got: {}",
27914            err.message
27915        );
27916    }
27917
27918    #[test]
27919    fn vector_using_sq8_display_roundtrips() {
27920        // The Display impl must produce text that re-parses to the
27921        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27922        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27923        let Statement::CreateTable(c) = s else {
27924            panic!()
27925        };
27926        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27927    }
27928
27929    #[test]
27930    fn parser_recognises_placeholders() {
27931        use crate::ast::{Expr, SelectItem, Statement};
27932        // $N in expression position parses as Expr::Placeholder(N).
27933        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27934        let Statement::Select(sel) = s else { panic!() };
27935        assert!(matches!(
27936            sel.items[0],
27937            SelectItem::Expr {
27938                expr: Expr::Placeholder(1),
27939                alias: None
27940            }
27941        ));
27942        // $2 + 1
27943        let SelectItem::Expr {
27944            expr: Expr::Binary { lhs, rhs, .. },
27945            ..
27946        } = &sel.items[1]
27947        else {
27948            panic!()
27949        };
27950        assert!(matches!(**lhs, Expr::Placeholder(2)));
27951        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27952        // WHERE x = $3
27953        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27954            panic!()
27955        };
27956        assert!(matches!(**rhs, Expr::Placeholder(3)));
27957    }
27958
27959    #[test]
27960    fn parser_rejects_dollar_zero() {
27961        // $0 is not valid in PG; the lexer rejects it.
27962        assert!(parse_statement("SELECT $0").is_err());
27963    }
27964
27965    #[test]
27966    fn placeholder_display_roundtrips() {
27967        // The Display impl must produce text that re-lexes to the
27968        // same Placeholder token.
27969        let s = parse("SELECT $42 FROM t");
27970        let printed = s.to_string();
27971        assert!(printed.contains("$42"));
27972        let again = parse(&printed);
27973        assert_eq!(s, again);
27974    }
27975
27976    #[test]
27977    fn alter_index_rebuild_bare() {
27978        use crate::ast::{AlterIndexTarget, Statement};
27979        let s = parse("ALTER INDEX my_idx REBUILD");
27980        let Statement::AlterIndex(a) = s else {
27981            panic!("expected AlterIndex, got {s:?}")
27982        };
27983        assert_eq!(a.name, "my_idx");
27984        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27985    }
27986
27987    #[test]
27988    fn alter_index_rebuild_with_encoding() {
27989        use crate::ast::{AlterIndexTarget, Statement};
27990        for (sql, want) in [
27991            (
27992                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27993                VecEncoding::F32,
27994            ),
27995            (
27996                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27997                VecEncoding::Sq8,
27998            ),
27999            (
28000                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28001                VecEncoding::F16,
28002            ),
28003        ] {
28004            let s = parse(sql);
28005            let Statement::AlterIndex(a) = s else {
28006                panic!("{sql}: expected AlterIndex")
28007            };
28008            assert_eq!(a.name, "my_idx");
28009            assert_eq!(
28010                a.target,
28011                AlterIndexTarget::Rebuild {
28012                    encoding: Some(want)
28013                },
28014                "{sql}"
28015            );
28016        }
28017    }
28018
28019    #[test]
28020    fn alter_index_rebuild_unknown_encoding_errors() {
28021        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
28022        assert!(
28023            err.message.contains("unknown vector encoding"),
28024            "got: {}",
28025            err.message
28026        );
28027    }
28028
28029    #[test]
28030    fn alter_index_rebuild_display_roundtrips() {
28031        for (input, want) in [
28032            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
28033            (
28034                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28035                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28036            ),
28037            (
28038                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28039                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28040            ),
28041        ] {
28042            let s = parse(input);
28043            assert_eq!(s.to_string(), want);
28044        }
28045    }
28046
28047    #[test]
28048    fn create_table_unknown_type_defers_to_engine() {
28049        // v4.9 picked XML as a parse-time "unsupported column
28050        // type" probe. v7.17.0 Phase 1.4 changed the contract:
28051        // an unknown type ident parses as Text + `user_type_ref`
28052        // so CREATE TABLE can resolve user-defined enum / domain
28053        // types — rejection of truly-unknown types moved to the
28054        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
28055        // to a first-class built-in, so this probe switched to a
28056        // synthetic name nothing in the lexer will ever recognise.
28057        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
28058        let Statement::CreateTable(t) = stmt else {
28059            panic!("expected CreateTable");
28060        };
28061        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
28062    }
28063
28064    #[test]
28065    fn create_table_missing_table_keyword_errors() {
28066        assert!(parse_statement("CREATE x (a INT)").is_err());
28067    }
28068
28069    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
28070    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
28071
28072    #[test]
28073    fn parse_create_table_partition_by_range() {
28074        use crate::ast::{PartitionBySpec, PartitionKindAst};
28075        let stmt = parse_statement(
28076            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
28077             payload JSONB) PARTITION BY RANGE (ts)",
28078        )
28079        .unwrap();
28080        let Statement::CreateTable(t) = stmt else {
28081            panic!("expected CreateTable");
28082        };
28083        assert!(t.partition_of.is_none(), "parent has no partition_of");
28084        assert_eq!(t.columns.len(), 3);
28085        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
28086        assert_eq!(
28087            by,
28088            &PartitionBySpec {
28089                kind: PartitionKindAst::Range,
28090                key_columns: alloc::vec!["ts".to_string()],
28091            }
28092        );
28093        // Display round-trip preserves the suffix. `quote_ident`
28094        // only adds double quotes when the ident needs escaping, so
28095        // a plain `ts` survives bare here.
28096        assert!(
28097            t.to_string().contains("PARTITION BY RANGE (ts)"),
28098            "Display lost PARTITION BY suffix: {t}"
28099        );
28100    }
28101
28102    #[test]
28103    fn parse_create_table_partition_of_range() {
28104        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
28105        let stmt = parse_statement(
28106            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
28107             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
28108        )
28109        .unwrap();
28110        let Statement::CreateTable(t) = stmt else {
28111            panic!("expected CreateTable");
28112        };
28113        assert!(t.columns.is_empty(), "child inherits columns from parent");
28114        assert!(t.partition_by.is_none());
28115        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28116        assert_eq!(of.parent_name, "events_partitioned");
28117        let PartitionOfSpec { bounds, .. } = of.clone();
28118        match bounds {
28119            PartitionOfBoundsAst::Range { lower, upper } => {
28120                assert!(lower.to_string().contains("2026-06-01"));
28121                assert!(upper.to_string().contains("2026-07-01"));
28122            }
28123            other => panic!("expected Range, got {other:?}"),
28124        }
28125        // Display round-trip emits the FOR VALUES tail. `quote_ident`
28126        // skips quotes when not required, so the parent name appears
28127        // bare here.
28128        let s = t.to_string();
28129        assert!(
28130            s.contains("PARTITION OF events_partitioned"),
28131            "Display lost PARTITION OF: {s}"
28132        );
28133        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
28134        assert!(s.contains(") TO ("), "Display lost TO: {s}");
28135    }
28136
28137    #[test]
28138    fn parse_create_table_partition_of_default() {
28139        use crate::ast::PartitionOfBoundsAst;
28140        let stmt =
28141            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
28142                .unwrap();
28143        let Statement::CreateTable(t) = stmt else {
28144            panic!("expected CreateTable");
28145        };
28146        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28147        assert_eq!(of.parent_name, "events_partitioned");
28148        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
28149        assert!(
28150            t.to_string()
28151                .contains("PARTITION OF events_partitioned DEFAULT"),
28152            "Display lost DEFAULT: {t}"
28153        );
28154    }
28155
28156    #[test]
28157    fn parse_create_table_partition_by_list() {
28158        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
28159        // child with `FOR VALUES IN (lit, lit, …)`.
28160        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28161        let parent =
28162            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
28163                .unwrap();
28164        let Statement::CreateTable(t) = parent else {
28165            panic!("expected CreateTable");
28166        };
28167        let Some(PartitionBySpec {
28168            kind,
28169            ref key_columns,
28170        }) = t.partition_by
28171        else {
28172            panic!("expected PARTITION BY");
28173        };
28174        assert_eq!(kind, PartitionKindAst::List);
28175        assert_eq!(*key_columns, vec!["region".to_string()]);
28176        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
28177
28178        let child = parse_statement(
28179            "CREATE TABLE events_apac PARTITION OF events_listed \
28180             FOR VALUES IN ('jp', 'kr', 'tw')",
28181        )
28182        .unwrap();
28183        let Statement::CreateTable(c) = child else {
28184            panic!("expected CreateTable");
28185        };
28186        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28187        let PartitionOfBoundsAst::List { values } = &of.bounds else {
28188            panic!("expected List bounds, got {:?}", of.bounds);
28189        };
28190        assert_eq!(values.len(), 3);
28191        let disp = c.to_string();
28192        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
28193    }
28194
28195    #[test]
28196    fn parse_create_table_partition_by_hash() {
28197        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
28198        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
28199        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28200        let parent =
28201            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
28202        let Statement::CreateTable(t) = parent else {
28203            panic!("expected CreateTable");
28204        };
28205        let Some(PartitionBySpec {
28206            kind,
28207            ref key_columns,
28208        }) = t.partition_by
28209        else {
28210            panic!("expected PARTITION BY");
28211        };
28212        assert_eq!(kind, PartitionKindAst::Hash);
28213        assert_eq!(*key_columns, vec!["id".to_string()]);
28214        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
28215
28216        let child = parse_statement(
28217            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
28218             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
28219        )
28220        .unwrap();
28221        let Statement::CreateTable(c) = child else {
28222            panic!("expected CreateTable");
28223        };
28224        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28225        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
28226            panic!("expected Hash bounds");
28227        };
28228        assert_eq!(modulus, 4);
28229        assert_eq!(remainder, 0);
28230        let disp = c.to_string();
28231        assert!(
28232            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
28233            "Display lost HASH bounds: {disp}"
28234        );
28235
28236        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
28237        let bad = parse_statement(
28238            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
28239             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
28240        );
28241        let msg = format!("{}", bad.unwrap_err());
28242        assert!(
28243            msg.contains("REMAINDER") && msg.contains("MODULUS"),
28244            "expected REMAINDER/MODULUS validation error: {msg}"
28245        );
28246    }
28247
28248    #[test]
28249    fn parse_create_table_partition_of_rejects_columns() {
28250        // v7.37.6-B contract: PARTITION OF children inherit columns
28251        // from the parent; an explicit list MUST surface as a parse
28252        // error rather than getting silently ignored.
28253        let err = parse_statement(
28254            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
28255             FOR VALUES FROM ('a') TO ('b')",
28256        );
28257        assert!(err.is_err(), "expected parse error for explicit columns");
28258        let msg = format!("{}", err.unwrap_err());
28259        assert!(
28260            msg.contains("PARTITION OF") && msg.contains("column"),
28261            "error should mention PARTITION OF + columns: {msg}"
28262        );
28263    }
28264
28265    #[test]
28266    fn insert_single_value() {
28267        let s = parse("INSERT INTO foo VALUES (42)");
28268        let Statement::Insert(i) = s else {
28269            panic!("expected Insert")
28270        };
28271        assert_eq!(i.table, "foo");
28272        assert_eq!(i.rows.len(), 1);
28273        assert_eq!(i.rows[0].len(), 1);
28274        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
28275    }
28276
28277    #[test]
28278    fn insert_multi_value_with_mixed_literals() {
28279        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
28280        let Statement::Insert(i) = s else { panic!() };
28281        assert_eq!(i.rows.len(), 1);
28282        assert_eq!(i.rows[0].len(), 5);
28283    }
28284
28285    #[test]
28286    fn insert_missing_into_errors() {
28287        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
28288    }
28289
28290    #[test]
28291    fn create_table_round_trip() {
28292        let original =
28293            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
28294        let text = original.to_string();
28295        let again = parse_statement(&text).expect("re-parse");
28296        assert_eq!(original, again);
28297    }
28298
28299    #[test]
28300    fn insert_round_trip_with_negation_and_string() {
28301        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28302        let text = original.to_string();
28303        let again = parse_statement(&text).expect("re-parse");
28304        assert_eq!(original, again);
28305    }
28306
28307    #[test]
28308    fn unknown_keyword_at_statement_start_errors() {
28309        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28310        // the top-level dispatch still has no branch to take.
28311        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28312        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28313    }
28314
28315    // --- v0.8 CREATE INDEX --------------------------------------------------
28316
28317    #[test]
28318    fn create_index_basic() {
28319        let s = parse("CREATE INDEX idx_id ON users (id)");
28320        let Statement::CreateIndex(c) = s else {
28321            panic!("expected CreateIndex")
28322        };
28323        assert_eq!(c.name, "idx_id");
28324        assert_eq!(c.table, "users");
28325        assert_eq!(c.column, "id");
28326    }
28327
28328    #[test]
28329    fn create_index_missing_on_errors() {
28330        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28331    }
28332
28333    #[test]
28334    fn create_index_missing_paren_errors() {
28335        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28336    }
28337
28338    #[test]
28339    fn create_index_round_trip() {
28340        let original = parse("CREATE INDEX by_name ON users (name)");
28341        let again = parse_statement(&original.to_string()).unwrap();
28342        assert_eq!(original, again);
28343    }
28344
28345    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28346
28347    #[test]
28348    fn create_unique_index_basic() {
28349        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28350        let Statement::CreateIndex(c) = s else {
28351            panic!("expected CreateIndex");
28352        };
28353        assert!(c.is_unique);
28354        assert_eq!(c.column, "a");
28355        assert!(c.partial_predicate.is_none());
28356    }
28357
28358    #[test]
28359    fn create_unique_index_partial() {
28360        // mailrs's email_templates "one default per user" shape.
28361        let s = parse(
28362            "CREATE UNIQUE INDEX idx_email_templates_user_default \
28363             ON email_templates (user_address) WHERE is_default = true",
28364        );
28365        let Statement::CreateIndex(c) = s else {
28366            panic!("expected CreateIndex");
28367        };
28368        assert!(c.is_unique);
28369        assert_eq!(c.table, "email_templates");
28370        assert_eq!(c.column, "user_address");
28371        assert!(c.partial_predicate.is_some());
28372    }
28373
28374    #[test]
28375    fn create_unique_index_composite_with_predicate() {
28376        // mailrs's calendar_events instance: composite columns.
28377        let s = parse(
28378            "CREATE UNIQUE INDEX uq_calendar_events_instance \
28379             ON calendar_events (calendar_id, uid, recurrence_id) \
28380             WHERE recurrence_id IS NOT NULL",
28381        );
28382        let Statement::CreateIndex(c) = s else {
28383            panic!("expected CreateIndex");
28384        };
28385        assert!(c.is_unique);
28386        assert_eq!(c.column, "calendar_id");
28387        assert_eq!(
28388            c.extra_columns,
28389            vec!["uid".to_string(), "recurrence_id".to_string()]
28390        );
28391        assert!(c.partial_predicate.is_some());
28392    }
28393
28394    #[test]
28395    fn create_unique_index_using_btree_ok() {
28396        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28397        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28398    }
28399
28400    #[test]
28401    fn create_unique_index_using_hnsw_rejected() {
28402        let err =
28403            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28404        assert!(err.message.contains("UNIQUE"), "{}", err.message);
28405    }
28406
28407    #[test]
28408    fn create_unique_index_round_trip() {
28409        let original = parse(
28410            "CREATE UNIQUE INDEX uq_calendar_events_master \
28411             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28412        );
28413        let again = parse_statement(&original.to_string()).unwrap();
28414        assert_eq!(original, again);
28415    }
28416
28417    #[test]
28418    fn create_unique_without_index_errors() {
28419        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28420        // v7.39 (round 340, V56) — PG 18.4, verbatim.
28421        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28422    }
28423
28424    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28425
28426    #[test]
28427    fn create_table_bytea_column() {
28428        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28429        let Statement::CreateTable(c) = s else {
28430            panic!("expected CreateTable");
28431        };
28432        assert_eq!(c.columns.len(), 2);
28433        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28434        assert!(!c.columns[1].nullable);
28435    }
28436
28437    #[test]
28438    fn create_table_bytes_alias_column() {
28439        let s = parse("CREATE TABLE t (blob BYTES)");
28440        let Statement::CreateTable(c) = s else {
28441            panic!("expected CreateTable");
28442        };
28443        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28444    }
28445
28446    #[test]
28447    fn bytea_round_trip_display() {
28448        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28449        let again = parse_statement(&original.to_string()).unwrap();
28450        assert_eq!(original, again);
28451    }
28452
28453    // --- v0.9 transactions -------------------------------------------------
28454
28455    #[test]
28456    fn begin_commit_rollback_parse_as_unit_variants() {
28457        assert_eq!(parse("BEGIN"), Statement::Begin(None));
28458        assert_eq!(parse("COMMIT"), Statement::Commit);
28459        // r1066 — PG synonyms pgbench's tpcb script relies on.
28460        assert_eq!(parse("END"), Statement::Commit);
28461        assert_eq!(parse("END TRANSACTION"), Statement::Commit);
28462        assert_eq!(parse("COMMIT WORK"), Statement::Commit);
28463        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28464        // Trailing semicolons accepted too.
28465        assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28466        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28467        // statement (with or without the WORK/TRANSACTION noise word).
28468        assert_eq!(
28469            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28470            Statement::Begin(Some(IsolationLevel::RepeatableRead))
28471        );
28472        assert_eq!(
28473            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28474            Statement::Begin(Some(IsolationLevel::Serializable))
28475        );
28476        // A non-isolation mode keeps the session default (None).
28477        assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28478    }
28479
28480    // --- v1.2: pgvector distance ops + ::vector cast --------------------
28481
28482    #[test]
28483    fn inner_product_binop_parses() {
28484        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28485        let Statement::Select(s) = s else { panic!() };
28486        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28487            panic!()
28488        };
28489        assert!(matches!(
28490            expr,
28491            Expr::Binary {
28492                op: BinOp::InnerProduct,
28493                ..
28494            }
28495        ));
28496    }
28497
28498    #[test]
28499    fn cosine_distance_binop_parses() {
28500        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28501        let Statement::Select(s) = s else { panic!() };
28502        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28503            panic!()
28504        };
28505        assert!(matches!(
28506            expr,
28507            Expr::Binary {
28508                op: BinOp::CosineDistance,
28509                ..
28510            }
28511        ));
28512    }
28513
28514    #[test]
28515    fn vector_cast_postfix_wraps_string_literal() {
28516        let s = parse("SELECT '[1,2,3]'::vector FROM t");
28517        let Statement::Select(s) = s else { panic!() };
28518        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28519            panic!()
28520        };
28521        assert!(matches!(
28522            expr,
28523            Expr::Cast {
28524                target: CastTarget::Vector,
28525                ..
28526            }
28527        ));
28528    }
28529
28530    #[test]
28531    fn unsupported_cast_target_errors() {
28532        // v7.37.5 ship triage promoted the parser to accept every
28533        // ident as a `CastTarget::Named(canonical)`; the engine
28534        // surfaces the "unsupported cast target" error at eval
28535        // time when `type_name_to_data_type` can't resolve it.
28536        // Parser-side error now requires a NON-ident after `::`
28537        // (e.g. a punctuation token).
28538        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28539        assert_eq!(err.message, "syntax error at or near \",\"");
28540    }
28541
28542    #[test]
28543    fn tx_statements_round_trip() {
28544        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28545            let original = parse(q);
28546            let again = parse_statement(&original.to_string()).unwrap();
28547            assert_eq!(original, again);
28548        }
28549    }
28550
28551    #[test]
28552    fn interval_text_parsing_units() {
28553        // v7.37.5 β — three-field shape `(months, days, micros)` so
28554        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28555        // Single unit.
28556        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28557        assert_eq!(
28558            parse_interval_text("24 hours"),
28559            Some((0, 0, 86_400_000_000))
28560        );
28561        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28562        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28563        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28564        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28565        // Compound spans accumulate per-dimension.
28566        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28567        assert_eq!(
28568            parse_interval_text("1 day 2 hours"),
28569            Some((0, 1, 7_200_000_000))
28570        );
28571        // Negative numbers carry through per-dimension.
28572        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28573        // Bad shapes return None.
28574        assert_eq!(parse_interval_text(""), None);
28575        assert_eq!(parse_interval_text("garbage"), None);
28576        assert_eq!(parse_interval_text("1 fortnight"), None);
28577        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28578        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28579        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28580        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28581        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28582    }
28583
28584    #[test]
28585    fn interval_literal_roundtrips_via_display() {
28586        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28587        let s = parsed.to_string();
28588        // Display preserves the original text verbatim.
28589        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28590        // And re-parsing yields a structurally equal statement.
28591        let again = parse_statement(&s).unwrap();
28592        assert_eq!(parsed, again);
28593    }
28594
28595    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28596
28597    #[test]
28598    fn parser_recognises_create_publication_bare() {
28599        let s = parse("CREATE PUBLICATION pub_a");
28600        let Statement::CreatePublication(p) = s else {
28601            panic!("expected CreatePublication, got {s:?}")
28602        };
28603        assert_eq!(p.name, "pub_a");
28604        assert_eq!(p.scope, PublicationScope::AllTables);
28605    }
28606
28607    #[test]
28608    fn parser_recognises_create_publication_for_all_tables() {
28609        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28610        let Statement::CreatePublication(p) = s else {
28611            panic!("expected CreatePublication, got {s:?}")
28612        };
28613        assert_eq!(p.name, "pub_a");
28614        assert_eq!(p.scope, PublicationScope::AllTables);
28615    }
28616
28617    #[test]
28618    fn parser_recognises_drop_publication() {
28619        let s = parse("DROP PUBLICATION pub_a");
28620        let Statement::DropPublication { name, .. } = s else {
28621            panic!("expected DropPublication, got {s:?}")
28622        };
28623        assert_eq!(name, "pub_a");
28624    }
28625
28626    #[test]
28627    fn parser_recognises_for_table_list() {
28628        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28629        let Statement::CreatePublication(p) = s else {
28630            panic!("expected CreatePublication, got {s:?}")
28631        };
28632        assert_eq!(p.name, "pub_a");
28633        let PublicationScope::ForTables(ts) = p.scope else {
28634            panic!("expected ForTables scope")
28635        };
28636        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28637    }
28638
28639    #[test]
28640    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28641        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28642        // is rejected (`invalid publication object list`; the old
28643        // test pinned an unverifiable "PG 19 accepts both" claim);
28644        // TABLES pairs with IN SCHEMA.
28645        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28646            .expect_err("bare FOR TABLES must reject");
28647        assert!(
28648            alloc::format!("{err}").contains("invalid publication object list"),
28649            "got: {err}"
28650        );
28651        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28652        let Statement::CreatePublication(p) = s else {
28653            panic!("expected CreatePublication, got {s:?}")
28654        };
28655        let PublicationScope::TablesInSchema(schema) = p.scope else {
28656            panic!("expected TablesInSchema")
28657        };
28658        assert_eq!(schema, "public");
28659    }
28660
28661    #[test]
28662    fn parser_recognises_for_all_tables_except_list() {
28663        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28664        let Statement::CreatePublication(p) = s else {
28665            panic!()
28666        };
28667        let PublicationScope::AllTablesExcept(ts) = p.scope else {
28668            panic!("expected AllTablesExcept")
28669        };
28670        assert_eq!(ts, alloc::vec!["t1", "t2"]);
28671    }
28672
28673    #[test]
28674    fn parser_rejects_for_table_with_empty_list() {
28675        // `FOR TABLE` with nothing after is a parse error.
28676        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28677            .expect_err("must error on empty list");
28678        // No specific message asserted — the call falls through to
28679        // expect_ident_like which yields "expected identifier, got …".
28680        assert!(!err.message.is_empty());
28681    }
28682
28683    #[test]
28684    fn parser_recognises_show_publications() {
28685        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28686        // bare ident in this position, NOT a reserved keyword.
28687        let s = parse("SHOW PUBLICATIONS");
28688        assert!(matches!(s, Statement::ShowPublications));
28689    }
28690
28691    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28692
28693    #[test]
28694    fn parser_recognises_create_subscription_single_publication() {
28695        let s = parse(
28696            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28697        );
28698        let Statement::CreateSubscription(c) = s else {
28699            panic!("expected CreateSubscription, got {s:?}")
28700        };
28701        assert_eq!(c.name, "sub_a");
28702        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28703        assert_eq!(c.publications, alloc::vec!["pub_a"]);
28704    }
28705
28706    #[test]
28707    fn parser_recognises_create_subscription_multi_publication() {
28708        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28709        let Statement::CreateSubscription(c) = s else {
28710            panic!()
28711        };
28712        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28713    }
28714
28715    #[test]
28716    fn parser_rejects_create_subscription_missing_connection() {
28717        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28718            .expect_err("must error on missing CONNECTION");
28719        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28720    }
28721
28722    #[test]
28723    fn parser_rejects_create_subscription_missing_publication() {
28724        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28725            .expect_err("must error on missing PUBLICATION");
28726        assert_eq!(err.message, "syntax error at end of input");
28727    }
28728
28729    #[test]
28730    fn parser_recognises_drop_subscription() {
28731        let s = parse("DROP SUBSCRIPTION sub_a");
28732        let Statement::DropSubscription { name, .. } = s else {
28733            panic!("expected DropSubscription, got {s:?}")
28734        };
28735        assert_eq!(name, "sub_a");
28736    }
28737
28738    #[test]
28739    fn parser_recognises_show_subscriptions() {
28740        let s = parse("SHOW SUBSCRIPTIONS");
28741        assert!(matches!(s, Statement::ShowSubscriptions));
28742    }
28743
28744    #[test]
28745    fn parser_recognises_wait_for_wal_position_no_timeout() {
28746        let s = parse("WAIT FOR WAL POSITION 12345");
28747        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28748            panic!("expected WaitForWalPosition, got {s:?}")
28749        };
28750        assert_eq!(pos, 12345);
28751        assert!(timeout_ms.is_none());
28752    }
28753
28754    #[test]
28755    fn parser_recognises_wait_for_wal_position_with_timeout() {
28756        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28757        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28758            panic!()
28759        };
28760        assert_eq!(pos, 67890);
28761        assert_eq!(timeout_ms, Some(5000));
28762    }
28763
28764    #[test]
28765    fn parser_rejects_wait_with_negative_position() {
28766        // The lexer treats `-` as a token; `expect_u64_literal`
28767        // only sees the Integer that follows, so the negative
28768        // arrives as a unary-minus expression at higher levels.
28769        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28770        // parse error one way or another.
28771        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28772        assert!(!err.message.is_empty());
28773    }
28774
28775    #[test]
28776    fn parser_recognises_bare_analyze() {
28777        let s = parse("ANALYZE");
28778        assert!(matches!(s, Statement::Analyze(None)));
28779    }
28780
28781    #[test]
28782    fn parser_recognises_analyze_with_table() {
28783        let s = parse("ANALYZE users");
28784        let Statement::Analyze(Some(name)) = s else {
28785            panic!("expected Analyze, got {s:?}")
28786        };
28787        assert_eq!(name, "users");
28788    }
28789
28790    #[test]
28791    fn parser_recognises_analyze_with_quoted_table() {
28792        let s = parse("ANALYZE \"Mixed Case\"");
28793        let Statement::Analyze(Some(name)) = s else {
28794            panic!()
28795        };
28796        assert_eq!(name, "Mixed Case");
28797    }
28798
28799    #[test]
28800    fn parser_rejects_analyze_with_garbage_token() {
28801        let err = parse_statement("ANALYZE 42").expect_err("must error");
28802        assert!(!err.message.is_empty());
28803    }
28804
28805    #[test]
28806    fn analyze_display_roundtrips() {
28807        for sql in ["ANALYZE", "ANALYZE users"] {
28808            let s = parse(sql);
28809            let printed = s.to_string();
28810            let again = parse_statement(&printed)
28811                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28812            assert_eq!(s, again);
28813        }
28814    }
28815
28816    #[test]
28817    fn wait_for_display_roundtrips() {
28818        for sql in [
28819            "WAIT FOR WAL POSITION 12345",
28820            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28821        ] {
28822            let s = parse(sql);
28823            let printed = s.to_string();
28824            let again = parse_statement(&printed)
28825                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28826            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28827        }
28828    }
28829
28830    #[test]
28831    fn subscription_ddl_display_roundtrips() {
28832        for sql in [
28833            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28834            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28835            "DROP SUBSCRIPTION sub_a",
28836            "SHOW SUBSCRIPTIONS",
28837        ] {
28838            let s = parse(sql);
28839            let printed = s.to_string();
28840            let again = parse_statement(&printed)
28841                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28842            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28843        }
28844    }
28845
28846    #[test]
28847    fn parser_drop_dispatches_user_vs_publication() {
28848        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28849        // tokenises DROP. Both targets must still parse.
28850        let s = parse("DROP USER 'alice'");
28851        let Statement::DropUser { name, .. } = s else {
28852            panic!("expected DropUser, got {s:?}")
28853        };
28854        assert_eq!(name, "alice");
28855        // And DROP PUBLICATION lands the new variant.
28856        let s = parse("DROP PUBLICATION p1");
28857        assert!(matches!(s, Statement::DropPublication { .. }));
28858    }
28859
28860    #[test]
28861    fn publication_ddl_display_roundtrips() {
28862        // Every CREATE PUBLICATION variant must Display → parse →
28863        // same AST. v6.1.3 covers all three scope shapes.
28864        for sql in [
28865            "CREATE PUBLICATION pub_a",
28866            "CREATE PUBLICATION pub_a FOR ALL TABLES",
28867            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28868            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28869            "DROP PUBLICATION pub_a",
28870            "SHOW PUBLICATIONS",
28871        ] {
28872            let s = parse(sql);
28873            let printed = s.to_string();
28874            let again = parse_statement(&printed)
28875                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28876            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28877        }
28878    }
28879
28880    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28881
28882    #[test]
28883    fn create_function_returns_trigger_plpgsql_minimal() {
28884        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28885        let s = parse(sql);
28886        let Statement::CreateFunction(f) = s else {
28887            panic!("expected CreateFunction");
28888        };
28889        assert_eq!(f.name, "noop");
28890        assert!(!f.or_replace);
28891        assert!(f.args.is_empty());
28892        assert!(matches!(f.returns, FunctionReturn::Trigger));
28893        assert_eq!(f.language, "plpgsql");
28894        let FunctionBody::PlPgSql(block) = f.body else {
28895            panic!("expected PlPgSql body");
28896        };
28897        assert_eq!(block.statements.len(), 1);
28898        assert!(matches!(
28899            block.statements[0],
28900            PlPgSqlStmt::Return(ReturnTarget::New)
28901        ));
28902    }
28903
28904    #[test]
28905    fn create_function_or_replace_with_assignment() {
28906        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28907        // RETURN NEW.
28908        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28909BEGIN
28910  NEW.search_vector := to_tsvector('english', NEW.subject);
28911  RETURN NEW;
28912END;
28913$$";
28914        let s = parse(sql);
28915        let Statement::CreateFunction(f) = s else {
28916            panic!("expected CreateFunction");
28917        };
28918        assert!(f.or_replace);
28919        let FunctionBody::PlPgSql(block) = &f.body else {
28920            panic!("expected PlPgSql body");
28921        };
28922        assert_eq!(block.statements.len(), 2);
28923        // First statement: NEW.search_vector := to_tsvector(...)
28924        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28925            panic!("expected Assign as first stmt");
28926        };
28927        match target {
28928            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28929            other => panic!("expected NEW.col, got {other:?}"),
28930        }
28931        // Second statement: RETURN NEW
28932        assert!(matches!(
28933            block.statements[1],
28934            PlPgSqlStmt::Return(ReturnTarget::New)
28935        ));
28936    }
28937
28938    #[test]
28939    fn create_trigger_after_insert_or_update() {
28940        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28941        let s = parse(sql);
28942        let Statement::CreateTrigger(t) = s else {
28943            panic!("expected CreateTrigger");
28944        };
28945        assert_eq!(t.name, "tg");
28946        assert_eq!(t.table, "messages");
28947        assert_eq!(t.timing, TriggerTiming::After);
28948        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28949        assert_eq!(t.for_each, TriggerForEach::Row);
28950        assert_eq!(t.function, "update_sv");
28951    }
28952
28953    #[test]
28954    fn create_trigger_before_delete_execute_procedure_alias() {
28955        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28956        let sql =
28957            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28958        let s = parse(sql);
28959        let Statement::CreateTrigger(t) = s else {
28960            panic!("expected CreateTrigger");
28961        };
28962        assert_eq!(t.timing, TriggerTiming::Before);
28963        assert_eq!(t.events, vec![TriggerEvent::Delete]);
28964    }
28965
28966    #[test]
28967    fn drop_trigger_if_exists_round_trips() {
28968        // No parser support for DROP TRIGGER yet — added in v7.12.5
28969        // alongside the broader DROP …{IF EXISTS} cleanup. The
28970        // AST + Display impls are in place so we round-trip via
28971        // construction:
28972        let s = Statement::DropTrigger {
28973            name: "tg".into(),
28974            table: "messages".into(),
28975            if_exists: true,
28976        };
28977        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28978    }
28979
28980    #[test]
28981    fn trigger_ddl_display_roundtrips_through_parser() {
28982        // CREATE TRIGGER + its referenced CREATE FUNCTION must
28983        // Display → parse → same AST (modulo PL/pgSQL body
28984        // formatting which is parser-canonicalised).
28985        for sql in [
28986            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28987            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28988        ] {
28989            let s = parse(sql);
28990            let printed = s.to_string();
28991            let again = parse_statement(&printed)
28992                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28993            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28994        }
28995    }
28996}