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 (recorded delta).
362/// Kept out of the parse_expr recursion frame — see the call site.
363#[inline(never)]
364fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
365    match parse_decimal_literal(&s) {
366        Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
367        // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
368        // its exact value as a NumericBig.
369        None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
370        // v7.39 (read01 numeric.c) — expand the exponent form.
371        None => match expand_scientific_literal(&s) {
372            SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
373                Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
374                None if plain
375                    .split_once('.')
376                    .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
377                {
378                    Ok(Literal::NumericBig(plain))
379                }
380                None => s
381                    .parse::<f64>()
382                    .map(Literal::Float)
383                    .map_err(|_| format!("invalid numeric literal {s:?}")),
384            },
385            SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
386            SciExpanded::NotScientific => s
387                .parse::<f64>()
388                .map(Literal::Float)
389                .map_err(|_| format!("invalid numeric literal {s:?}")),
390        },
391    }
392}
393
394fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
395    let (int_part, frac_part) = match s.split_once('.') {
396        Some((i, f)) => (i, f),
397        None => (s, ""),
398    };
399    // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
400    // places fell out of the numeric path here, which is why
401    // `pg_typeof(1e-256)` answered double precision and a plain
402    // 256-place decimal aborted the query in the big-decimal converter.
403    if frac_part.len() > u16::MAX as usize {
404        return None;
405    }
406    let mut digits = String::with_capacity(int_part.len() + frac_part.len());
407    digits.push_str(int_part);
408    digits.push_str(frac_part);
409    let mantissa: i128 = digits.parse().ok()?;
410    #[allow(clippy::cast_possible_truncation)]
411    Some((mantissa, frac_part.len() as u16))
412}
413
414/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
415/// record-returning JSON functions that take a `AS alias(col type, …)`
416/// column-definition list in FROM position.
417fn is_json_to_record_name(s: &str) -> bool {
418    s.eq_ignore_ascii_case("jsonb_to_recordset")
419        || s.eq_ignore_ascii_case("jsonb_to_record")
420        // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
421        // column-definition list desugars identically (the record base
422        // argument only carries the type; a non-NULL base's field
423        // defaults are a recorded delta).
424        || s.eq_ignore_ascii_case("json_populate_record")
425        || s.eq_ignore_ascii_case("jsonb_populate_record")
426        || s.eq_ignore_ascii_case("json_populate_recordset")
427        || s.eq_ignore_ascii_case("jsonb_populate_recordset")
428        || s.eq_ignore_ascii_case("json_to_recordset")
429        || s.eq_ignore_ascii_case("json_to_record")
430}
431
432fn is_vector_opclass_name(name: &str) -> bool {
433    let lc = name.to_ascii_lowercase();
434    matches!(
435        lc.as_str(),
436        "vector_cosine_ops"
437            | "vector_l2_ops"
438            | "vector_ip_ops"
439            | "halfvec_cosine_ops"
440            | "halfvec_l2_ops"
441            | "halfvec_ip_ops"
442            | "sq8_cosine_ops"
443            | "sq8_l2_ops"
444            | "sq8_ip_ops"
445            // pg_trgm — trigram operator class. SPG's GIN index
446            // already uses tsvector tokens; trigram-style LIKE
447            // pattern matching still routes through a sequential
448            // scan, but the opclass name is accepted so PG schemas
449            // load.
450            | "gin_trgm_ops"
451            | "gist_trgm_ops"
452            // PG built-in btree opclasses occasionally appear in
453            // pg_dump output for column types with multiple
454            // sort orders (text_pattern_ops, varchar_pattern_ops,
455            // bpchar_pattern_ops).
456            | "text_pattern_ops"
457            | "varchar_pattern_ops"
458            | "bpchar_pattern_ops"
459            | "int4_ops"
460            | "int8_ops"
461            | "text_ops"
462    )
463}
464
465#[derive(Debug, Clone, PartialEq, Eq)]
466pub struct ParseError {
467    pub message: String,
468    /// Index into the token stream where parsing tripped. Not a byte offset.
469    /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
470    /// field would grow every `Result<_, ParseError>` slot on the deeply
471    /// recursive parse stack and tip the nesting-budget frame cliff. PG's
472    /// 1-based char position is recovered on the cold error path by
473    /// [`syntax_error_position`], which re-tokenizes to map this token index.
474    pub token_pos: usize,
475}
476
477impl fmt::Display for ParseError {
478    /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
479    /// with `parse error at token #N: `, which PG has no equivalent of:
480    /// the message bodies are already PG's verbatim (`LIMIT must not be
481    /// negative`, `invalid input syntax for type bigint: "abc"`), and the
482    /// prefix was SPG's internal token index leaking into every one of
483    /// them. `token_pos` stays a field — the wire recovers PG's 1-based
484    /// character position from it for the ErrorResponse `P`.
485    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
486        f.write_str(&self.message)
487    }
488}
489
490impl From<LexError> for ParseError {
491    fn from(e: LexError) -> Self {
492        Self {
493            message: format!("lex: {e}"),
494            token_pos: 0,
495        }
496    }
497}
498
499/// v7.9.30 — parse a single expression (no trailing junk). Used by
500/// the engine to re-hydrate stored partial-index / unique-index
501/// predicates from their canonical Display form. The same Pratt
502/// parser the statement path uses; this entry point just skips the
503/// statement dispatch.
504pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
505    let (tokens, offsets) =
506        lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
507    let mut p = Parser::new(tokens);
508    let expr = p
509        .parse_expr(0)
510        .and_then(|e| p.expect_eof().map(|()| e))
511        .map_err(|e| shape_syntax_error(e, input, &offsets))?;
512    Ok(expr)
513}
514
515/// Parse exactly one statement, swallow an optional trailing `;`, and require
516/// the token stream to end there. PG string semantics.
517pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
518    parse_statement_with(input, false)
519}
520
521/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
522/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
523/// The engine threads its session flag through here.
524pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
525    let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
526        .map_err(|e| shape_lex_error(&e, input))?;
527    // The same session flag names the dialect for both the lexer and
528    // the type mapping.
529    let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
530    let stmt = (|| {
531        let stmt = p.parse_one_statement()?;
532        if matches!(p.peek(), Token::Semicolon) {
533            p.advance();
534        }
535        p.expect_eof()?;
536        Ok(stmt)
537    })()
538    .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
539    Ok(stmt)
540}
541
542/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
543/// `syntax error at or near "<token>"` and `syntax error at end of input`
544/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
545/// prose — `expected identifier, got Eof`, `unexpected token From in
546/// expression`, `expected end of input, got Ident("with")` — which named
547/// internal token types and, in the Debug forms, leaked the parser's own
548/// enum into a message clients read.
549///
550/// Applied once on the way out, so every construction site is covered and
551/// the token named is the one the error itself points at. Messages whose
552/// bodies are already PG's verbatim (`LIMIT must not be negative`,
553/// `invalid input syntax for type bigint: "abc"`) are left alone — those
554/// are PG's own errors, not its syntax error.
555fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
556    if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
557        return e;
558    }
559    let message = match offending_lexeme(input, offsets, e.token_pos) {
560        Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
561        None => "syntax error at end of input".into(),
562    };
563    ParseError {
564        message,
565        token_pos: e.token_pos,
566    }
567}
568
569/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
570/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
571/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
572/// comment at or near "/* x"` — the quoted part runs from the opening
573/// delimiter to the end of the input. SPG reported its own internal
574/// shape instead (`unterminated string literal at byte 7`), which named
575/// a byte offset no client can use.
576fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
577    use lexer::LexErrorKind as K;
578    let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
579    let message = match &e.kind {
580        K::UnterminatedString => {
581            alloc::format!("unterminated quoted string at or near \"{from_here}\"")
582        }
583        K::UnterminatedQuotedIdent => {
584            alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
585        }
586        K::UnterminatedBlockComment => {
587            alloc::format!("unterminated /* comment at or near \"{from_here}\"")
588        }
589        // PG has no "unknown character" error of its own — the character
590        // is skipped and the parser reports the next token. SPG stops at
591        // the character itself and names it, which is the same shape.
592        K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
593        // The number-literal kinds already carry PG's `at or near` form.
594        other => alloc::format!(
595            "{}",
596            lexer::LexError {
597                kind: other.clone(),
598                pos: e.pos,
599            }
600        ),
601    };
602    ParseError {
603        message,
604        token_pos: 0,
605    }
606}
607
608/// The offending token exactly as it appears in the input, or `None` at
609/// end of input. PG echoes the source spelling — a lower-case `frm`
610/// reports as `frm`, not as a canonicalised keyword.
611fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
612    let start = *offsets.get(token_pos)?;
613    if start >= input.len() {
614        return None;
615    }
616    let end = offsets
617        .get(token_pos + 1)
618        .copied()
619        .unwrap_or(input.len())
620        .min(input.len());
621    let seg = input.get(start..end)?.trim();
622    if seg.is_empty() {
623        return None;
624    }
625    // A quoted literal / identifier keeps its inner spaces; anything else
626    // ends at the first whitespace (the segment runs to the NEXT token's
627    // start, which may swallow a comment).
628    if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
629        Some(seg)
630    } else {
631        seg.split_whitespace().next()
632    }
633}
634
635/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
636/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
637/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
638/// this re-tokenizes `input` on the cold error path to map the failing token
639/// index to its start byte, then to a character offset. `backslash_escapes`
640/// must match the parse that produced `token_pos` (it barely shifts offsets,
641/// but stay consistent). Returns `None` when the index has no offset or the
642/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
643#[must_use]
644pub fn syntax_error_position(
645    input: &str,
646    backslash_escapes: bool,
647    token_pos: usize,
648) -> Option<usize> {
649    let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
650    let byte_off = *offsets.get(token_pos)?;
651    if byte_off > input.len() || !input.is_char_boundary(byte_off) {
652        return None;
653    }
654    Some(input[..byte_off].chars().count() + 1)
655}
656
657struct Parser {
658    tokens: Vec<Token>,
659    pos: usize,
660    /// v7.39 (round 274) — the session's dialect, carried by the same
661    /// signal that drives string-literal escaping: `SET sql_mode` (only
662    /// MySQL clients and mysqldump preambles emit it) turns it on,
663    /// `SET standard_conforming_strings` (every pg_dump preamble) turns
664    /// it off. Needed here because the two dialects disagree about what
665    /// `REAL` means — see the type mapping below.
666    mysql_dialect: bool,
667    /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
668    /// mutually recursive expr/select parsers. Bounded so a deeply
669    /// nested input returns a parse error instead of overflowing
670    /// the stack (embed hosts die on overflow — it is an abort,
671    /// not a catchable error).
672    nest_depth: usize,
673    /// TABLESAMPLE lowering channel: the table-ref parser pushes a
674    /// `random() < p/100` predicate here; the enclosing SELECT
675    /// drains the list after its WHERE parses and ANDs the
676    /// predicates in. parse_bare_select save/restores around its
677    /// FROM+WHERE so nested selects only drain their own.
678    pending_sample_preds: Vec<Expr>,
679    /// v7.39 (round 691) — collation lowering channel, the same shape as
680    /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
681    /// information, and `ast::OrderBy` is where this parser keeps ordering
682    /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
683    /// variant — puts a new arm on `eval_expr`, which this repo has
684    /// measured to overflow the debug stack. So while an ORDER BY KEY is
685    /// being parsed the postfix loop drops the name here instead of
686    /// refusing it, and the key's parser takes it.
687    ///
688    /// Only inside an ORDER BY key: everywhere else an unperformable
689    /// collation still errors, because accepting one at a COMPARISON and
690    /// ignoring it is the defect F36 exists to close.
691    in_order_by_key: bool,
692    order_key_collation: Option<String>,
693    /// POSITION(sub IN str) — while parsing the needle, the IN
694    /// keyword is the argument separator, not a membership test.
695    /// The postfix loop leaves IN unconsumed when this is set.
696    suppress_in_tail: bool,
697    /// Index of the token the last `advance()` returned — see
698    /// [`Parser::consumed_pos`].
699    last_consumed: usize,
700    /// v7.39 (round 506) — the statement's own text and the byte each token
701    /// starts at, so a MySQL projection item can report the SOURCE TEXT
702    /// MariaDB reports: `SELECT a  +  b` names its column `a  +  b`,
703    /// spacing and all. Only filled for a MySQL session — a PG one names
704    /// columns from the parsed shape and pays nothing for this.
705    src: Option<(String, Vec<usize>)>,
706}
707
708/// Max expr/select parser nesting (parens, subqueries, CASE, …).
709/// Real SQL nests a few dozen levels at the extreme. Each nesting level
710/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
711/// exists to turn a deep statement into a catchable parse ERROR: a stack
712/// overflow is an abort, and in the server it does not fail one query, it
713/// takes the process down and every other connection with it.
714///
715/// v7.39 (round 507) — measured, because the figure here used to be a
716/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
717/// in BOTH debug and release"), and the debug half of that is wrong by
718/// more than an order of magnitude:
719///
720///   * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
721///     this budget and errors. Verified against a live server for nested
722///     derived tables, parens, calls, CASE, IN-subqueries, scalar
723///     subqueries, NOT and unary minus — the server stayed up through all
724///     of them. This is the contract that matters, and it holds.
725///   * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
726///     LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
727///     and executing aborts around 8 inside a test thread. The budget is
728///     simply unreachable there, which is why a deep-nesting test has to
729///     ask for a large stack of its own — see `nesting_budget_errors_at`
730///     in the parser tests.
731/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
732/// one place.
733///
734/// There were two copies of this fact: a curated list, used for BARE
735/// names, and — in `try_peek_meta_qualified` — no list at all, which
736/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
737/// the engine to complain about a view it could not materialise. So
738/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
739/// had rows, `pg_catalog.pg_stat_activity` was an error.
740///
741/// PG puts `pg_catalog` at the implicit front of every search_path, so
742/// the two spellings name the same relation and must resolve the same
743/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
744/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
745/// meta_view_result path under their own names and must not be
746/// rewritten; a name that is neither reaches the ordinary resolver,
747/// which reports that the relation does not exist — PG's answer.
748const SYNTHESISED_PG_CATALOGS: &[&str] = &[
749    "pg_am",
750    "pg_attrdef",
751    "pg_attribute",
752    "pg_cast",
753    "pg_db_role_setting",
754    "pg_conversion",
755    "pg_default_acl",
756    "pg_shadow",
757    "pg_sequences",
758    "pg_range",
759    "pg_partitioned_table",
760    "pg_language",
761    "pg_group",
762    "pg_authid",
763    "pg_class",
764    "pg_collation",
765    "pg_constraint",
766    "pg_database",
767    "pg_depend",
768    // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
769    "pg_description",
770    "pg_enum",
771    "pg_extension",
772    // v7.39 (round 541) — pg_dump reads it for every relation of kind
773    // 'f'. SPG has no foreign tables, so it is empty, which is also
774    // what PG reports on a database that has none.
775    "pg_foreign_table",
776    // v7.39 (round 541) — the empty-by-truth family; see
777    // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
778    "pg_event_trigger",
779    "pg_file_settings",
780    "pg_foreign_data_wrapper",
781    "pg_foreign_server",
782    "pg_hba_file_rules",
783    "pg_ident_file_mappings",
784    "pg_init_privs",
785    "pg_parameter_acl",
786    "pg_prepared_xacts",
787    "pg_publication_namespace",
788    "pg_publication_rel",
789    "pg_publication_tables",
790    "pg_replication_origin",
791    "pg_replication_origin_status",
792    "pg_seclabel",
793    "pg_seclabels",
794    "pg_shdepend",
795    "pg_shdescription",
796    "pg_shmem_allocations",
797    "pg_shmem_allocations_numa",
798    "pg_shseclabel",
799    "pg_statistic_ext_data",
800    "pg_stats_ext",
801    "pg_stats_ext_exprs",
802    "pg_subscription_rel",
803    "pg_transform",
804    "pg_user_mapping",
805    "pg_user_mappings",
806    "pg_index",
807    "pg_indexes",
808    "pg_inherits",
809    // v7.39 (round 650) — the text-search catalogs SPG can fill
810    // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
811    // token types to dictionaries and SPG has no token-type model,
812    // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
813    "pg_ts_config",
814    "pg_ts_config_map",
815    "pg_ts_dict",
816    "pg_ts_parser",
817    "pg_ts_template",
818    "pg_matviews",
819    "pg_namespace",
820    // v7.39 (round 621)
821    "pg_operator",
822    "pg_policies",
823    "pg_policy",
824    "pg_proc",
825    "pg_publication",
826    "pg_replication_slots",
827    "pg_roles",
828    // v7.39 (round 143) — the rewrite-rule listing view.
829    // v7.39 (round 312) — and the rule catalogue itself, which
830    // `pg_get_ruledef(oid)` resolves against.
831    "pg_rewrite",
832    "pg_rules",
833    "pg_sequence",
834    "pg_settings",
835    "pg_stat_archiver",
836    "pg_stat_bgwriter",
837    "pg_stat_checkpointer",
838    "pg_stat_database",
839    "pg_stat_io",
840    "pg_stat_progress_analyze",
841    "pg_auth_members",
842    "pg_stat_progress_create_index",
843    "pg_stat_progress_vacuum",
844    "pg_stat_replication",
845    "pg_stat_slru",
846    "pg_stat_subscription_stats",
847    "pg_stat_user_functions",
848    "pg_stat_user_indexes",
849    "pg_stat_user_tables",
850    "pg_stat_wal",
851    "pg_prepared_statements",
852    "pg_largeobject",
853    "pg_largeobject_metadata",
854    "pg_statistic",
855    "pg_statistic_ext",
856    "pg_subscription",
857    "pg_tables",
858    "pg_tablespace",
859    // v7.39 (round 502) — the timezone catalogues. SPG resolved
860    // named zones correctly but could not list them, so a client
861    // populating a timezone picker got "relation does not exist".
862    "pg_timezone_abbrevs",
863    "pg_timezone_names",
864    "pg_trigger",
865    "pg_type",
866    "pg_user",
867    "pg_views",
868];
869
870const MAX_NEST_DEPTH: usize = 64;
871
872/// Stack accounting for the nesting budget, test-only.
873///
874/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
875/// that MOVES: a compiler upgrade grew the parser's debug frames and
876/// silently ate the margin until `nesting_budget_errors_cleanly` went
877/// from erroring cleanly to aborting on a stack overflow. A count
878/// cannot notice that on its own, so the budget is measured here and
879/// held to a ceiling.
880///
881/// The reading has to come from a helper whose OWN frame is the same at
882/// every call: debug slot placement does not follow source order, so a
883/// local's address inside the function under test is not that
884/// function's frame boundary. Two earlier probes were wrong that way —
885/// one read `&self.nest_depth`, which is the `Parser`'s address and
886/// never moves at all.
887#[cfg(test)]
888mod frame_meter {
889    extern crate std;
890    use std::cell::Cell;
891
892    // Per-THREAD, not global. `cargo test` runs tests in parallel and
893    // plenty of them parse nested expressions, so shared statics get
894    // stack addresses from several threads at once and the subtraction
895    // below turns into noise — it read 229,772 bytes per level that way,
896    // while passing when the test was run on its own.
897    std::thread_local! {
898        static AT_LO: Cell<usize> = const { Cell::new(0) };
899        static AT_HI: Cell<usize> = const { Cell::new(0) };
900    }
901
902    pub(super) const SAMPLE_LO: usize = 4;
903    pub(super) const SAMPLE_HI: usize = 24;
904
905    #[inline(never)]
906    pub(super) fn record(depth: usize) {
907        let anchor = 0u8;
908        let at = core::ptr::from_ref(&anchor) as usize;
909        if depth == SAMPLE_LO {
910            AT_LO.with(|c| c.set(at));
911        } else if depth == SAMPLE_HI {
912            AT_HI.with(|c| c.set(at));
913        }
914    }
915
916    /// Bytes of stack one nesting level costs, averaged over the span.
917    pub(super) fn bytes_per_level() -> usize {
918        let lo = AT_LO.with(Cell::get);
919        let hi = AT_HI.with(Cell::get);
920        assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
921        assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
922        (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
923    }
924
925    pub(super) fn reset() {
926        AT_LO.with(|c| c.set(0));
927        AT_HI.with(|c| c.set(0));
928    }
929}
930
931/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
932/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
933#[inline(never)]
934fn build_center_call(e: Expr) -> Expr {
935    Expr::FunctionCall {
936        name: alloc::string::String::from("center"),
937        args: alloc::vec![e],
938    }
939}
940
941/// Max consecutive binary operators at ONE precedence level
942/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
943/// parse time but evaluates and drops recursively — depth beyond
944/// this overflows 2 MiB worker stacks (debug eval frames run
945/// multiple KiB). `IN (…)` lists are flat and unaffected.
946const MAX_BINARY_CHAIN: usize = 256;
947
948/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
949/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
950/// it keeps its dedicated path (`parse_table_level_fk`).
951enum NamedTableConstraintKind {
952    Check,
953    Unique,
954    PrimaryKey,
955    Exclude,
956}
957
958impl Parser {
959    fn new(tokens: Vec<Token>) -> Self {
960        Self::new_with_dialect(tokens, false)
961    }
962
963    fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
964        Self {
965            tokens,
966            mysql_dialect,
967            in_order_by_key: false,
968            order_key_collation: None,
969            pos: 0,
970            nest_depth: 0,
971            pending_sample_preds: Vec::new(),
972            suppress_in_tail: false,
973            last_consumed: 0,
974            src: None,
975        }
976    }
977
978    /// Hand the parser the text it is parsing, for [`Parser::source_span`].
979    fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
980        if self.mysql_dialect {
981            self.src = Some((input.to_string(), offsets.to_vec()));
982        }
983        self
984    }
985
986    /// The source text spanning tokens `start ..= end`, trimmed.
987    ///
988    /// The offsets are token STARTS, so the span runs to the start of the
989    /// token after `end` and gives back the whitespace between them —
990    /// trimming is what makes `a + b FROM t` end at `b`.
991    fn source_span(&self, start: usize, end: usize) -> Option<&str> {
992        let (text, offsets) = self.src.as_ref()?;
993        let from = *offsets.get(start)?;
994        let to = *offsets.get(end + 1)?;
995        text.get(from..to).map(str::trim_end)
996    }
997
998    /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
999    /// nesting depth, erroring out cleanly past the budget.
1000    fn enter_nested(&mut self) -> Result<(), ParseError> {
1001        self.nest_depth += 1;
1002        #[cfg(test)]
1003        frame_meter::record(self.nest_depth);
1004        if self.nest_depth > MAX_NEST_DEPTH {
1005            self.nest_depth -= 1;
1006            return Err(self.err(alloc::format!(
1007                "statement nests deeper than {MAX_NEST_DEPTH} levels"
1008            )));
1009        }
1010        Ok(())
1011    }
1012
1013    fn peek(&self) -> &Token {
1014        // tokens always ends with Eof; pos is clamped in advance().
1015        &self.tokens[self.pos]
1016    }
1017
1018    fn advance(&mut self) -> Token {
1019        let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1020        self.last_consumed = self.pos;
1021        if self.pos + 1 < self.tokens.len() {
1022            self.pos += 1;
1023        }
1024        t
1025    }
1026
1027    /// v7.39 (round 340, V56) — the index of the token `advance()` just
1028    /// returned. It was computed as `pos - 1`, which is wrong at both
1029    /// ends: `advance()` parks on the final Eof rather than running off
1030    /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1031    /// input`), and after backtracking `pos` is no longer one past the
1032    /// token that failed. Recorded by `advance()` itself instead.
1033    const fn consumed_pos(&self) -> usize {
1034        self.last_consumed
1035    }
1036
1037    fn err(&self, message: String) -> ParseError {
1038        ParseError {
1039            message,
1040            token_pos: self.pos,
1041        }
1042    }
1043
1044    fn expect_eof(&self) -> Result<(), ParseError> {
1045        if matches!(self.peek(), Token::Eof) {
1046            Ok(())
1047        } else {
1048            Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1049        }
1050    }
1051
1052    /// v7.14.0 — swallow every token up to (but not including) the
1053    /// next semicolon / EOF. Used by the dump-noise dispatcher
1054    /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1055    /// etc. without modeling each grammar.
1056    /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1057    /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1058    /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1059    /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1060    /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1061    fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1062        let start = self.pos;
1063        self.advance(); // COMMENT
1064        if !matches!(self.peek(), Token::On) {
1065            self.pos = start;
1066            self.consume_until_statement_boundary();
1067            return Ok(Statement::Empty);
1068        }
1069        self.advance(); // ON
1070        let kind = match self.peek() {
1071            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1072            Token::Table => "table".into(),
1073            _ => {
1074                self.consume_until_statement_boundary();
1075                return Ok(Statement::Empty);
1076            }
1077        };
1078        if !matches!(
1079            kind.as_str(),
1080            "table"
1081                | "column"
1082                | "index"
1083                | "view"
1084                | "sequence"
1085                | "schema"
1086                | "type"
1087                | "database"
1088                | "function"
1089        ) {
1090            self.consume_until_statement_boundary();
1091            return Ok(Statement::Empty);
1092        }
1093        self.advance(); // the kind keyword
1094        // The object name. ⚠️ `expect_ident_like` strips a leading
1095        // `<schema>.` qualifier and returns only the trailing ident (SPG is
1096        // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1097        // `c`. Read the dotted parts from raw tokens instead, then let a
1098        // 3-part `schema.t.c` drop its leading schema like everywhere else.
1099        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1100        loop {
1101            match self.advance() {
1102                Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1103                other if unreserved_keyword_text(&other).is_some() => {
1104                    parts.push(unreserved_keyword_text(&other).unwrap());
1105                }
1106                other => {
1107                    return Err(ParseError {
1108                        message: alloc::format!("expected identifier, got {other:?}"),
1109                        token_pos: self.consumed_pos(),
1110                    });
1111                }
1112            }
1113            if matches!(self.peek(), Token::Dot) {
1114                self.advance();
1115            } else {
1116                break;
1117            }
1118        }
1119        // COLUMN wants `table.column`; every other kind wants a bare name.
1120        let want = if kind == "column" { 2 } else { 1 };
1121        while parts.len() > want {
1122            parts.remove(0);
1123        }
1124        let name = parts.join(".");
1125        // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1126        // pg_dump writes the SIGNATURE, and the paren list was a syntax
1127        // error here — a dump carrying one function comment failed to
1128        // restore. The list is consumed (the comment store keys by name;
1129        // overload-precise comments are the function-predicate follow-up).
1130        if matches!(self.peek(), Token::LParen)
1131            && matches!(
1132                kind.as_str(),
1133                "function" | "procedure" | "aggregate" | "routine"
1134            )
1135        {
1136            let mut depth = 0usize;
1137            loop {
1138                match self.advance() {
1139                    Token::LParen => depth += 1,
1140                    Token::RParen => {
1141                        depth -= 1;
1142                        if depth == 0 {
1143                            break;
1144                        }
1145                    }
1146                    Token::Eof => {
1147                        return Err(self.err(alloc::string::String::from(
1148                            "unterminated argument list in COMMENT ON",
1149                        )));
1150                    }
1151                    _ => {}
1152                }
1153            }
1154        }
1155        // `IS`
1156        if !matches!(self.peek(), Token::Is) {
1157            self.expect_keyword_ident("is")?;
1158        } else {
1159            self.advance();
1160        }
1161        let comment = match self.peek() {
1162            Token::Null => {
1163                self.advance();
1164                None
1165            }
1166            _ => Some(self.expect_string_literal()?),
1167        };
1168        Ok(Statement::CommentOn {
1169            kind,
1170            name,
1171            comment,
1172        })
1173    }
1174
1175    /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1176    /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1177    /// [CASCADE|RESTRICT]`.
1178    ///
1179    /// TABLE privileges are the real ones (stored, enforced, introspectable).
1180    /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1181    /// and the no-ON `GRANT role TO role` membership form — parses into
1182    /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1183    /// on them still restores.
1184    fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1185        self.advance(); // GRANT / REVOKE
1186        // REVOKE's optional `GRANT OPTION FOR` prefix.
1187        let mut grant_option = false;
1188        if !grant
1189            && self.peek_keyword_ident("grant")
1190            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1191        {
1192            self.advance(); // GRANT
1193            self.advance(); // OPTION
1194            self.expect_keyword_ident("for")?;
1195            grant_option = true;
1196        }
1197        // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1198        // words each with an optional COLUMN list.
1199        let mut privileges: Vec<GrantPriv> = Vec::new();
1200        if matches!(self.peek(), Token::All) {
1201            self.advance();
1202            if self.peek_keyword_ident("privileges") {
1203                self.advance();
1204            }
1205            // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1206            // column only.
1207            if matches!(self.peek(), Token::LParen) {
1208                let columns = self.parse_grant_column_list()?;
1209                privileges.push(GrantPriv {
1210                    word: "ALL".into(),
1211                    columns,
1212                });
1213            }
1214        } else {
1215            loop {
1216                // SELECT and INSERT lex as reserved tokens, so they never
1217                // reach `expect_ident_like` as plain idents; the rest
1218                // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1219                // MAINTAIN) are ordinary identifiers.
1220                let w = match self.peek() {
1221                    Token::Select => {
1222                        self.advance();
1223                        "SELECT".to_string()
1224                    }
1225                    Token::Insert => {
1226                        self.advance();
1227                        "INSERT".to_string()
1228                    }
1229                    // v7.39 (read01 round 60) — CREATE is a privilege word on a
1230                    // schema / database, and it lexes as a reserved token.
1231                    Token::Create => {
1232                        self.advance();
1233                        "CREATE".to_string()
1234                    }
1235                    // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1236                    // alice`) these "privilege words" are ROLE NAMES, and a
1237                    // role name is case-sensitive. `priv_from_word` folds case
1238                    // itself when they really are privileges.
1239                    _ => self.expect_ident_like()?,
1240                };
1241                // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1242                // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1243                let columns = if matches!(self.peek(), Token::LParen) {
1244                    self.parse_grant_column_list()?
1245                } else {
1246                    Vec::new()
1247                };
1248                privileges.push(GrantPriv { word: w, columns });
1249                if matches!(self.peek(), Token::Comma) {
1250                    self.advance();
1251                } else {
1252                    break;
1253                }
1254            }
1255        }
1256        // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1257        // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1258        if !matches!(self.peek(), Token::On) {
1259            let roles: Vec<String> = core::mem::take(&mut privileges)
1260                .into_iter()
1261                .map(|p| p.word)
1262                .collect();
1263            let grantees = self.parse_grantee_list(grant)?;
1264            // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1265            // no admin-option layer: a member cannot re-grant).
1266            self.consume_until_statement_boundary();
1267            return Ok(finish_grant(
1268                grant,
1269                GrantStatement {
1270                    privileges: Vec::new(),
1271                    object: GrantObject::Roles(roles),
1272                    grantees,
1273                    grant_option,
1274                },
1275            ));
1276        }
1277        self.advance(); // ON
1278        // An optional object-class keyword. `TABLE` (or no keyword at all) is
1279        // the enforced case; anything else parses and no-ops.
1280        let mut class = "TABLE";
1281        match self.peek() {
1282            Token::Table => {
1283                self.advance();
1284            }
1285            Token::All => {
1286                // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1287                // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1288                // IN SCHEMA` stay no-ops and keep their own object class.
1289                self.advance(); // ALL
1290                let kind = match self.peek() {
1291                    Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1292                    // TABLES has its own token (SHOW TABLES owns it).
1293                    Token::Tables | Token::Table => "tables".to_string(),
1294                    _ => String::new(),
1295                };
1296                if !kind.is_empty() {
1297                    self.advance();
1298                }
1299                // `IN SCHEMA <name>`
1300                if matches!(self.peek(), Token::In) {
1301                    self.advance();
1302                    if self.peek_keyword_ident("schema") {
1303                        self.advance();
1304                        let _schema = self.expect_ident_like()?;
1305                    }
1306                }
1307                if kind != "tables" {
1308                    self.consume_until_statement_boundary();
1309                    return Ok(finish_grant(
1310                        grant,
1311                        GrantStatement {
1312                            privileges,
1313                            object: GrantObject::Other("ALL … IN SCHEMA".into()),
1314                            grantees: Vec::new(),
1315                            grant_option,
1316                        },
1317                    ));
1318                }
1319                let grantees = self.parse_grantee_list(grant)?;
1320                self.consume_until_statement_boundary();
1321                return Ok(finish_grant(
1322                    grant,
1323                    GrantStatement {
1324                        privileges,
1325                        object: GrantObject::AllTablesInSchema,
1326                        grantees,
1327                        grant_option,
1328                    },
1329                ));
1330            }
1331            Token::Ident(w) | Token::QuotedIdent(w) => {
1332                let lc = w.to_ascii_lowercase();
1333                // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1334                // real objects with real ACLs now.
1335                if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1336                    self.advance();
1337                    let mut names: Vec<String> = Vec::new();
1338                    loop {
1339                        let mut parts: Vec<String> = Vec::new();
1340                        loop {
1341                            parts.push(self.expect_ident_like()?);
1342                            if matches!(self.peek(), Token::Dot) {
1343                                self.advance();
1344                            } else {
1345                                break;
1346                            }
1347                        }
1348                        names.push(parts.pop().expect("at least one part"));
1349                        if matches!(self.peek(), Token::Comma) {
1350                            self.advance();
1351                        } else {
1352                            break;
1353                        }
1354                    }
1355                    let grantees = self.parse_grantee_list(grant)?;
1356                    let mut grant_option = grant_option;
1357                    if grant && self.peek_keyword_ident("with") {
1358                        self.advance();
1359                        self.expect_keyword_ident("grant")?;
1360                        self.expect_keyword_ident("option")?;
1361                        grant_option = true;
1362                    }
1363                    self.consume_until_statement_boundary();
1364                    let object = match lc.as_str() {
1365                        "sequence" => GrantObject::Sequences(names),
1366                        "schema" => GrantObject::Schemas(names),
1367                        _ => GrantObject::Databases(names),
1368                    };
1369                    return Ok(finish_grant(
1370                        grant,
1371                        GrantStatement {
1372                            privileges,
1373                            object,
1374                            grantees,
1375                            grant_option,
1376                        },
1377                    ));
1378                }
1379                // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1380                // keys functions by NAME, so the argument list parses and is
1381                // dropped (an overload set shares one ACL — recorded residual).
1382                if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1383                    self.advance();
1384                    let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1385                    loop {
1386                        let mut parts: Vec<String> = Vec::new();
1387                        loop {
1388                            parts.push(self.expect_ident_like()?);
1389                            if matches!(self.peek(), Token::Dot) {
1390                                self.advance();
1391                            } else {
1392                                break;
1393                            }
1394                        }
1395                        let fname = parts.pop().expect("at least one part");
1396                        // v7.39 (read01 round 62) — the signature picks the
1397                        // overload, so it is captured.
1398                        let sig = if matches!(self.peek(), Token::LParen) {
1399                            Some(self.parse_function_signature_types()?)
1400                        } else {
1401                            None
1402                        };
1403                        names.push((fname, sig));
1404                        if matches!(self.peek(), Token::Comma) {
1405                            self.advance();
1406                        } else {
1407                            break;
1408                        }
1409                    }
1410                    let grantees = self.parse_grantee_list(grant)?;
1411                    self.consume_until_statement_boundary();
1412                    return Ok(finish_grant(
1413                        grant,
1414                        GrantStatement {
1415                            privileges,
1416                            object: GrantObject::Functions(names),
1417                            grantees,
1418                            grant_option,
1419                        },
1420                    ));
1421                }
1422                if matches!(
1423                    lc.as_str(),
1424                    "type"
1425                        | "domain"
1426                        | "language"
1427                        | "tablespace"
1428                        | "large"
1429                        | "foreign"
1430                        | "parameter"
1431                ) {
1432                    self.consume_until_statement_boundary();
1433                    return Ok(finish_grant(
1434                        grant,
1435                        GrantStatement {
1436                            privileges,
1437                            object: GrantObject::Other(lc.to_ascii_uppercase()),
1438                            grantees: Vec::new(),
1439                            grant_option,
1440                        },
1441                    ));
1442                }
1443                class = "TABLE";
1444            }
1445            _ => {}
1446        }
1447        let _ = class;
1448        // The table list. Schema-qualified names drop their qualifier (SPG is
1449        // single-schema) — but read the dotted parts from raw tokens, since
1450        // `expect_ident_like` would silently swallow the leading part.
1451        let mut tables: Vec<String> = Vec::new();
1452        loop {
1453            let mut parts: Vec<String> = Vec::new();
1454            loop {
1455                parts.push(self.expect_ident_like()?);
1456                if matches!(self.peek(), Token::Dot) {
1457                    self.advance();
1458                } else {
1459                    break;
1460                }
1461            }
1462            tables.push(parts.pop().expect("at least one part"));
1463            if matches!(self.peek(), Token::Comma) {
1464                self.advance();
1465            } else {
1466                break;
1467            }
1468        }
1469        let grantees = self.parse_grantee_list(grant)?;
1470        if grant && self.peek_keyword_ident("with") {
1471            self.advance();
1472            self.expect_keyword_ident("grant")?;
1473            self.expect_keyword_ident("option")?;
1474            grant_option = true;
1475        }
1476        // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1477        // to cascade to (no re-granting), so both are accepted and ignored.
1478        if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1479            self.advance();
1480        }
1481        Ok(finish_grant(
1482            grant,
1483            GrantStatement {
1484                privileges,
1485                object: GrantObject::Tables(tables),
1486                grantees,
1487                grant_option,
1488            },
1489        ))
1490    }
1491
1492    /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1493    /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1494    /// words; the caller normalises them into a signature key.
1495    fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1496        self.advance(); // (
1497        let mut types: Vec<String> = Vec::new();
1498        if matches!(self.peek(), Token::RParen) {
1499            self.advance();
1500            return Ok(types);
1501        }
1502        loop {
1503            // Collect the words of one argument up to a comma / close paren.
1504            let mut words: Vec<String> = Vec::new();
1505            loop {
1506                match self.peek() {
1507                    Token::Comma | Token::RParen | Token::Eof => break,
1508                    _ => {}
1509                }
1510                let tok = self.advance();
1511                match tok {
1512                    Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1513                    other => {
1514                        if let Some(w) = unreserved_keyword_text(&other) {
1515                            words.push(w);
1516                        }
1517                    }
1518                }
1519            }
1520            // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1521            // themselves several words (`double precision`, `character
1522            // varying`, `timestamp with time zone`), so "two words means the
1523            // first is a parameter name" reads the type off `f(double
1524            // precision)` as `precision`. v7.39 (round 282): recognise the
1525            // multi-word spellings first — a leading word that STARTS one of
1526            // them is part of the type, not a name.
1527            let joined = words.join(" ");
1528            let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1529                joined
1530            } else if words.len() >= 2 {
1531                words[1..].join(" ")
1532            } else {
1533                words.first().cloned().unwrap_or_default()
1534            };
1535            types.push(ty);
1536            if matches!(self.peek(), Token::Comma) {
1537                self.advance();
1538            } else {
1539                break;
1540            }
1541        }
1542        if matches!(self.peek(), Token::RParen) {
1543            self.advance();
1544        }
1545        Ok(types)
1546    }
1547
1548    /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1549    fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1550        self.advance(); // (
1551        let mut cols = Vec::new();
1552        loop {
1553            cols.push(self.expect_ident_like()?);
1554            if matches!(self.peek(), Token::Comma) {
1555                self.advance();
1556            } else {
1557                break;
1558            }
1559        }
1560        if !matches!(self.peek(), Token::RParen) {
1561            return Err(self.err(alloc::format!(
1562                "expected ')' to close the column list, got {:?}",
1563                self.peek()
1564            )));
1565        }
1566        self.advance(); // )
1567        Ok(cols)
1568    }
1569
1570    /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1571    /// PUBLIC.
1572    fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1573        if grant {
1574            if matches!(self.peek(), Token::To) {
1575                self.advance();
1576            } else {
1577                self.expect_keyword_ident("to")?;
1578            }
1579        } else if matches!(self.peek(), Token::From) {
1580            self.advance();
1581        } else {
1582            self.expect_keyword_ident("from")?;
1583        }
1584        let mut grantees: Vec<String> = Vec::new();
1585        loop {
1586            // `GROUP name` is the legacy spelling of a plain role name.
1587            if self.peek_keyword_ident("group") {
1588                self.advance();
1589            }
1590            if self.peek_keyword_ident("public") {
1591                self.advance();
1592                grantees.push(String::new()); // PUBLIC
1593            } else {
1594                grantees.push(self.expect_ident_like()?);
1595            }
1596            if matches!(self.peek(), Token::Comma) {
1597                self.advance();
1598            } else {
1599                break;
1600            }
1601        }
1602        Ok(grantees)
1603    }
1604
1605    /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1606    /// The body keeps its `$N` placeholders; substitution happens at
1607    /// EXECUTE. The declared types are recorded for
1608    /// `pg_prepared_statements.parameter_types` but are not enforced —
1609    /// PG infers when the list is omitted, and SPG resolves the values
1610    /// at substitution time either way.
1611    fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1612        let start = self.pos;
1613        self.advance(); // PREPARE
1614        // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1615        // different statement that happens to share the keyword. PG
1616        // ships with `max_prepared_transactions = 0` and reports it
1617        // this way; SPG has no prepared-transaction registry, so the
1618        // same wording is the accurate answer rather than a dodge.
1619        // Round 277 turned this from a silent no-op into a confusing
1620        // "expected AS in PREPARE" parse error.
1621        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1622            self.advance();
1623            let gid = match self.advance() {
1624                Token::String(g) => g,
1625                other => {
1626                    return Err(self.err(alloc::format!(
1627                        "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1628                    )));
1629                }
1630            };
1631            return Ok(Statement::PrepareTransaction(gid));
1632        }
1633        let name = self.expect_ident_like()?;
1634        let mut param_types = Vec::new();
1635        if matches!(self.peek(), Token::LParen) {
1636            self.advance();
1637            loop {
1638                let mut ty = self.expect_ident_like()?;
1639                // A parameterised type name (`numeric(10,2)`,
1640                // `varchar(20)`) keeps its argument list in the text.
1641                if matches!(self.peek(), Token::LParen) {
1642                    let mut depth = 0usize;
1643                    let mut buf = String::from("(");
1644                    loop {
1645                        match self.advance() {
1646                            Token::LParen => {
1647                                depth += 1;
1648                                if depth > 1 {
1649                                    buf.push('(');
1650                                }
1651                            }
1652                            Token::RParen => {
1653                                depth -= 1;
1654                                buf.push(')');
1655                                if depth == 0 {
1656                                    break;
1657                                }
1658                            }
1659                            Token::Comma => buf.push(','),
1660                            Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1661                            Token::Eof => break,
1662                            _ => {}
1663                        }
1664                    }
1665                    ty.push_str(&buf);
1666                }
1667                param_types.push(ty);
1668                match self.peek() {
1669                    Token::Comma => {
1670                        self.advance();
1671                    }
1672                    Token::RParen => {
1673                        self.advance();
1674                        break;
1675                    }
1676                    other => {
1677                        return Err(self.err(alloc::format!(
1678                            "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1679                        )));
1680                    }
1681                }
1682            }
1683        }
1684        if !matches!(self.peek(), Token::As) {
1685            return Err(self.err(alloc::format!(
1686                "expected AS in PREPARE, got {:?}",
1687                self.peek()
1688            )));
1689        }
1690        self.advance();
1691        let body = self.parse_one_statement()?;
1692        // The Parser holds tokens, not the source text, so the
1693        // statement PG reports in `pg_prepared_statements.statement`
1694        // is rebuilt from the AST rather than sliced from the input.
1695        let _ = start;
1696        let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1697        if !param_types.is_empty() {
1698            source.push_str(" (");
1699            source.push_str(&param_types.join(", "));
1700            source.push(')');
1701        }
1702        source.push_str(" AS ");
1703        source.push_str(&alloc::format!("{body}"));
1704        Ok(Statement::Prepare {
1705            name,
1706            param_types,
1707            body: alloc::boxed::Box::new(body),
1708            source,
1709        })
1710    }
1711
1712    /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1713    fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1714        self.advance(); // EXECUTE
1715        let name = self.expect_ident_like()?;
1716        let mut args = Vec::new();
1717        if matches!(self.peek(), Token::LParen) {
1718            self.advance();
1719            if matches!(self.peek(), Token::RParen) {
1720                self.advance();
1721            } else {
1722                loop {
1723                    args.push(self.parse_expr(0)?);
1724                    match self.advance() {
1725                        Token::Comma => {}
1726                        Token::RParen => break,
1727                        other => {
1728                            return Err(self.err(alloc::format!(
1729                                "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1730                            )));
1731                        }
1732                    }
1733                }
1734            }
1735        }
1736        Ok(Statement::Execute { name, args })
1737    }
1738
1739    /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1740    /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1741    /// procedure catalog yet, so this reports PG's not-found error
1742    /// (with its HINT) rather than pretending the call ran.
1743    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1744    /// Bare `DISCARD` is a syntax error in PG; so it is here.
1745    fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1746        self.advance(); // DISCARD
1747        let target = match self.advance() {
1748            Token::All => DiscardTarget::All,
1749            Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1750                "all" => DiscardTarget::All,
1751                "plans" => DiscardTarget::Plans,
1752                "sequences" => DiscardTarget::Sequences,
1753                "temp" | "temporary" => DiscardTarget::Temp,
1754                other => {
1755                    return Err(self.err(format!(
1756                        "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1757                    )));
1758                }
1759            },
1760            other => {
1761                return Err(self.err(format!(
1762                    "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1763                )));
1764            }
1765        };
1766        Ok(Statement::Discard(target))
1767    }
1768
1769    /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1770    /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1771    /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1772    /// aggressively the server interrupts, which SPG does not distinguish.
1773    /// Bare `KILL <id>` means CONNECTION.
1774    fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1775        self.advance(); // KILL
1776        let mut query_only = false;
1777        loop {
1778            // CONNECTION is a reserved keyword token (it also opens
1779            // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1780            // `Token::Connection` rather than a bare ident.
1781            if matches!(self.peek(), Token::Connection) {
1782                self.advance();
1783                break;
1784            }
1785            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1786                break;
1787            };
1788            match w.to_ascii_lowercase().as_str() {
1789                "hard" | "soft" => {
1790                    self.advance();
1791                }
1792                "query" => {
1793                    self.advance();
1794                    query_only = true;
1795                    break;
1796                }
1797                _ => break,
1798            }
1799        }
1800        let id = self.parse_expr(0)?;
1801        Ok(Statement::Kill {
1802            query_only,
1803            id: Box::new(id),
1804        })
1805    }
1806
1807    fn parse_call(&mut self) -> Result<Statement, ParseError> {
1808        self.advance(); // CALL
1809        let name = self.expect_ident_like()?;
1810        self.consume_until_statement_boundary();
1811        Ok(Statement::Call(name))
1812    }
1813
1814    fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1815        self.advance(); // DEALLOCATE
1816        // PG accepts an optional noise `PREPARE` keyword here.
1817        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1818            self.advance();
1819        }
1820        if matches!(self.peek(), Token::All) {
1821            self.advance();
1822            return Ok(Statement::Deallocate(None));
1823        }
1824        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1825            self.advance();
1826            return Ok(Statement::Deallocate(None));
1827        }
1828        let name = self.expect_ident_like()?;
1829        Ok(Statement::Deallocate(Some(name)))
1830    }
1831
1832    fn consume_until_statement_boundary(&mut self) {
1833        loop {
1834            match self.peek() {
1835                Token::Semicolon | Token::Eof => return,
1836                _ => self.advance(),
1837            };
1838        }
1839    }
1840
1841    /// v7.22 (round-13 T2) — consume to the statement boundary like
1842    /// `consume_until_statement_boundary`, but pick out the sequence
1843    /// name on the way: either `SEQUENCE NAME <ident>` (identity
1844    /// columns) or the first string literal (`nextval('<seq>')`).
1845    /// Schema qualifiers and `::regclass` casts are stripped.
1846    fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1847        let mut seq: Option<String> = None;
1848        let mut after_sequence_kw = false;
1849        let mut after_name_kw = false;
1850        loop {
1851            match self.peek().clone() {
1852                Token::Semicolon | Token::Eof => break,
1853                Token::Ident(s) | Token::QuotedIdent(s) => {
1854                    if after_name_kw && seq.is_none() {
1855                        self.advance();
1856                        let mut name = s;
1857                        // `SEQUENCE NAME public.groups_id_seq` — keep
1858                        // the bare name, drop qualifiers.
1859                        while matches!(self.peek(), Token::Dot) {
1860                            self.advance();
1861                            if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1862                                name = n;
1863                            }
1864                        }
1865                        seq = Some(name);
1866                        after_name_kw = false;
1867                        continue;
1868                    }
1869                    if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1870                        after_name_kw = true;
1871                        after_sequence_kw = false;
1872                    } else {
1873                        after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1874                    }
1875                    self.advance();
1876                }
1877                Token::String(s) => {
1878                    if seq.is_none() {
1879                        // `nextval('public.groups_id_seq'::regclass)`
1880                        let bare = s
1881                            .rsplit_once('.')
1882                            .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1883                        seq = Some(bare);
1884                    }
1885                    self.advance();
1886                }
1887                _ => {
1888                    after_sequence_kw = false;
1889                    after_name_kw = false;
1890                    self.advance();
1891                }
1892            }
1893        }
1894        seq
1895    }
1896
1897    /// v7.39 (round 621) — is the next token the keyword `BY`?
1898    ///
1899    /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
1900    /// column, table and alias name — and SPG lexed it into a dedicated
1901    /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
1902    /// two-letter keywords the lexer knew, this was the only one PG leaves
1903    /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
1904    ///
1905    /// The token is gone; the three clauses that own the word — GROUP BY,
1906    /// ORDER BY, PARTITION BY — and the handful of other places that expect it
1907    /// ask this instead. Adding it to the unreserved-identifier table was not
1908    /// enough on its own: identifier positions that match the token shape
1909    /// directly (an index's column list, a table alias) never consult that
1910    /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
1911    /// Not lexing it as a keyword closes the whole class rather than the two
1912    /// positions that happened to be noticed.
1913    fn peek_is_by(&self) -> bool {
1914        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
1915    }
1916
1917    /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
1918    /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
1919    /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
1920    fn consume_drop_behaviour(&mut self) {
1921        if matches!(
1922            self.peek(),
1923            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
1924        ) {
1925            self.advance();
1926        }
1927    }
1928
1929    fn expect_ident_like(&mut self) -> Result<String, ParseError> {
1930        let first = match self.advance() {
1931            Token::Ident(s) | Token::QuotedIdent(s) => s,
1932            // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
1933            // per PG's `pg_get_keywords()` classification. SPG tokenizes
1934            // these as named variants for parsing leverage in the
1935            // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
1936            // `BEGIN`, etc.), but they MUST still be usable as table /
1937            // column / alias names in DDL+DML. Sentori migrations like
1938            // 0001_init.sql ship `release TEXT NOT NULL` in the events
1939            // table — the `events.release` column carries the release
1940            // identifier string. Pre-T4 this triggered "expected
1941            // identifier, got Release" and blocked every drop-in user
1942            // whose schema had a column / alias with one of these names.
1943            other if unreserved_keyword_text(&other).is_some() => {
1944                unreserved_keyword_text(&other).unwrap()
1945            }
1946            other => {
1947                return Err(ParseError {
1948                    message: format!("expected identifier, got {other:?}"),
1949                    token_pos: self.consumed_pos(),
1950                });
1951            }
1952        };
1953        // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
1954        // qualify every name with `public.` (and pg_catalog.* for
1955        // functions); SPG is single-schema so we discard the
1956        // prefix and return only the trailing ident. Same shape
1957        // also handles MySQL `db.tbl` cross-database refs (SPG
1958        // ignores the db part).
1959        if matches!(self.peek(), Token::Dot) {
1960            self.advance();
1961            match self.advance() {
1962                Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
1963                other if unreserved_keyword_text(&other).is_some() => {
1964                    return Ok(unreserved_keyword_text(&other).unwrap());
1965                }
1966                other => {
1967                    return Err(ParseError {
1968                        message: format!("expected identifier after '{first}.', got {other:?}"),
1969                        token_pos: self.consumed_pos(),
1970                    });
1971                }
1972            }
1973        }
1974        Ok(first)
1975    }
1976
1977    #[allow(clippy::too_many_lines)]
1978    fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
1979        // v7.14.0 — empty / comment-only / semicolon-only input
1980        // (after the lexer strips line + block + MySQL
1981        // conditional comments) lands as Statement::Empty.
1982        // pg_dump and mysqldump emit several wrappers that
1983        // collapse to nothing after stripping (`/*!40101 SET …
1984        // */;`, blank lines between statements); the engine
1985        // returns CommandOk no-op so the dump loads cleanly.
1986        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
1987            return Ok(Statement::Empty);
1988        }
1989        // v7.14.0 — pg_dump / mysqldump "noise" statements:
1990        // catalog / metadata DDL that has no behavioural effect
1991        // on SPG's single-schema, single-database, single-user
1992        // model. Consume the whole statement up to the next
1993        // semicolon / EOF and return Empty. This is broader than
1994        // the per-keyword DROP / SET / COMMENT arms but lets the
1995        // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
1996        // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
1997        // `BEGIN; COMMIT;` wrappers, etc. all pass through.
1998        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
1999            let lc = s.to_ascii_lowercase();
2000            // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2001            if lc == "comment" {
2002                return self.parse_comment_on();
2003            }
2004            // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2005            if lc == "grant" || lc == "revoke" {
2006                return self.parse_grant_or_revoke(lc == "grant");
2007            }
2008            // v7.39 (round 277) — the SQL-level prepared-statement
2009            // surface is REAL now. It used to be accepted and dropped
2010            // on the theory that "real execution still happens via the
2011            // extended-query flow" — true only for a driver that uses
2012            // that flow; a plain SQL PREPARE / EXECUTE returned no
2013            // rows at all.
2014            if lc == "prepare" {
2015                return self.parse_prepare();
2016            }
2017            if lc == "execute" {
2018                return self.parse_execute();
2019            }
2020            if lc == "deallocate" {
2021                return self.parse_deallocate();
2022            }
2023            // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2024            // accepted and dropped, so an application's stored-procedure
2025            // invocation reported success and did nothing. SPG has no
2026            // procedure catalog, so every CALL names a procedure that
2027            // does not exist — which is exactly what PG says.
2028            if lc == "call" {
2029                return self.parse_call();
2030            }
2031            // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2032            // names one connection and acts on it.
2033            if lc == "kill" {
2034                return self.parse_kill();
2035            }
2036            if lc == "discard" {
2037                return self.parse_discard();
2038            }
2039            // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2040            // Still performs nothing; the roles are carried out so a name
2041            // that does not exist is refused, as PG18 refuses it.
2042            if lc == "reassign" {
2043                self.advance();
2044                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2045                    self.advance();
2046                }
2047                if self.peek_is_by() {
2048                    self.advance();
2049                }
2050                // Only the roles BEFORE the TO are the ones that must
2051                // exist — `TO` names the new owner, which PG checks as
2052                // well, so both lists are collected.
2053                let mut names = self.take_comma_separated_names();
2054                if matches!(self.peek(), Token::To) {
2055                    self.advance();
2056                    names.extend(self.take_comma_separated_names());
2057                }
2058                self.consume_until_statement_boundary();
2059                return Ok(Statement::ValidateOnly {
2060                    kind: crate::ast::ValidateOnlyKind::RoleName,
2061                    names,
2062                });
2063            }
2064            // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2065            // unconditionally with `no security label providers have been
2066            // loaded`, whatever object it names, because none is loaded.
2067            // SPG has none either; accepting it told the caller a label had
2068            // been applied when nothing anywhere records one.
2069            if lc == "security" {
2070                self.consume_until_statement_boundary();
2071                return Ok(Statement::ValidateOnly {
2072                    kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2073                    names: Vec::new(),
2074                });
2075            }
2076            if is_dump_noise_statement(&lc) {
2077                self.consume_until_statement_boundary();
2078                return Ok(Statement::Empty);
2079            }
2080        }
2081        match self.peek() {
2082            Token::Select => self.parse_select_stmt(),
2083            // v7.37.17 (17.6 siblings) — a statement opening with a
2084            // parenthesized query group: `(SELECT … UNION …)
2085            // INTERSECT …`. parse_bare_select's group arm consumes
2086            // the parens; the select parser handles the outer chain
2087            // and tail.
2088            Token::LParen
2089                if matches!(
2090                    self.tokens.get(self.pos + 1),
2091                    Some(Token::Select | Token::LParen | Token::Values)
2092                ) =>
2093            {
2094                self.parse_select_stmt()
2095            }
2096            // v7.37.17 (17.6 siblings) — top-level bare VALUES
2097            // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2098            // Lowers to the same UNION ALL chain the FROM-position
2099            // form uses, then reuses the shared SELECT tail.
2100            Token::Values => {
2101                self.advance(); // VALUES
2102                let mut head = self.parse_values_rows_body()?;
2103                self.parse_select_tail_into(&mut head)?;
2104                Ok(Statement::Select(head))
2105            }
2106            // SQL-standard `TABLE name` shorthand for
2107            // `SELECT * FROM name` — pg_dump never emits it, but
2108            // psql users and PG docs use it constantly. Set-op
2109            // chains and the ORDER BY/LIMIT tail compose like any
2110            // SELECT head.
2111            Token::Table
2112                if matches!(
2113                    self.tokens.get(self.pos + 1),
2114                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2115                ) =>
2116            {
2117                let mut head = self.parse_table_shorthand()?;
2118                self.parse_setop_chain_into(&mut head)?;
2119                self.parse_select_tail_into(&mut head)?;
2120                Ok(Statement::Select(head))
2121            }
2122            // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2123            // body is a dollar-quoted plpgsql block (lexer already
2124            // collapsed `$$…$$` into a single Token::String).
2125            // v7.16.2 — mailrs round-10 A.2: parse the body as a
2126            // real PlPgSqlBlock so the engine can EXECUTE it at
2127            // top level instead of silently swallowing. Pre-
2128            // v7.16.2 the parser threw the body away and the
2129            // engine returned CommandOk for the entire DO; that
2130            // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2131            // $$` into a SEV-1 silent no-op (the IF + the rename
2132            // were both invisible — mailrs's migrate-042 didn't
2133            // actually run). Now the body parses + executes;
2134            // EmbeddedSql inside the block runs immediately
2135            // against the engine (not deferred — we're at top
2136            // level, not inside a trigger row-write loop).
2137            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2138                self.advance();
2139                let body_text = match self.advance() {
2140                    Token::String(s) => s,
2141                    other => {
2142                        return Err(self.err(alloc::format!(
2143                            "expected dollar-quoted body after DO, got {other:?}"
2144                        )));
2145                    }
2146                };
2147                // Optional `LANGUAGE <name>` trailer (idents only).
2148                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2149                    self.advance();
2150                    let _ = self.expect_ident_like()?;
2151                }
2152                // Parse the body — same shape CREATE FUNCTION
2153                // uses for trigger function bodies. If the body
2154                // doesn't parse cleanly we surface the error
2155                // (better than silent no-op).
2156                let block = parse_plpgsql_body(&body_text)?;
2157                Ok(Statement::DoBlock(block))
2158            }
2159            // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2160            // WITH isn't a reserved token in our lexer — comes through
2161            // as `Token::Ident("with")` (case-insensitive).
2162            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2163                self.advance();
2164                self.parse_with_cte_then_select()
2165            }
2166            // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2167            // an identifier — not a reserved keyword.
2168            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2169                self.advance();
2170                let mut analyze = false;
2171                let mut suggest = false;
2172                let mut costs_off = false;
2173                let mut buffers = false;
2174                let mut timing_off = false;
2175                let mut settings = false;
2176                let mut wal = false;
2177                let mut summary_off = false;
2178                let mut format = crate::ast::ExplainFormat::Text;
2179                // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2180                // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2181                // options are comma-separated. Booleans default to ON
2182                // when the value token is omitted (matches PG).
2183                if matches!(self.peek(), Token::LParen) {
2184                    self.advance();
2185                    loop {
2186                        let opt = match self.peek().clone() {
2187                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2188                            other => {
2189                                return Err(self.err(format!(
2190                                    "expected option keyword inside EXPLAIN (…), got {other:?}"
2191                                )));
2192                            }
2193                        };
2194                        self.advance();
2195                        if opt.eq_ignore_ascii_case("suggest") {
2196                            suggest = true;
2197                            // SUGGEST takes no explicit value today.
2198                        } else if opt.eq_ignore_ascii_case("costs") {
2199                            // PG syntax: `COSTS [ON | OFF]`. Default
2200                            // when value omitted is ON, so plain
2201                            // `COSTS` is a no-op. `COSTS OFF` flips.
2202                            // `ON` lexes to `Token::On` (reserved
2203                            // keyword in JOIN ... ON contexts); accept
2204                            // it alongside the bare Ident form so the
2205                            // grammar matches PG verbatim.
2206                            let value = match self.peek().clone() {
2207                                Token::On => {
2208                                    self.advance();
2209                                    true
2210                                }
2211                                Token::Ident(v) | Token::QuotedIdent(v)
2212                                    if v.eq_ignore_ascii_case("off") =>
2213                                {
2214                                    self.advance();
2215                                    false
2216                                }
2217                                Token::Ident(v) | Token::QuotedIdent(v)
2218                                    if v.eq_ignore_ascii_case("true") =>
2219                                {
2220                                    self.advance();
2221                                    true
2222                                }
2223                                _ => true,
2224                            };
2225                            costs_off = !value;
2226                        } else if opt.eq_ignore_ascii_case("analyze")
2227                            || opt.eq_ignore_ascii_case("analyse")
2228                        {
2229                            // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2230                            // Same default-ON rule as ANALYZE keyword form.
2231                            let value = match self.peek().clone() {
2232                                Token::On => {
2233                                    self.advance();
2234                                    true
2235                                }
2236                                Token::Ident(v) | Token::QuotedIdent(v)
2237                                    if v.eq_ignore_ascii_case("off") =>
2238                                {
2239                                    self.advance();
2240                                    false
2241                                }
2242                                Token::Ident(v) | Token::QuotedIdent(v)
2243                                    if v.eq_ignore_ascii_case("true") =>
2244                                {
2245                                    self.advance();
2246                                    true
2247                                }
2248                                _ => true,
2249                            };
2250                            analyze = value;
2251                        } else if opt.eq_ignore_ascii_case("buffers") {
2252                            // v7.37.22 — `BUFFERS [ON|OFF]`.
2253                            let value = match self.peek().clone() {
2254                                Token::On => {
2255                                    self.advance();
2256                                    true
2257                                }
2258                                Token::Ident(v) | Token::QuotedIdent(v)
2259                                    if v.eq_ignore_ascii_case("off") =>
2260                                {
2261                                    self.advance();
2262                                    false
2263                                }
2264                                Token::Ident(v) | Token::QuotedIdent(v)
2265                                    if v.eq_ignore_ascii_case("true") =>
2266                                {
2267                                    self.advance();
2268                                    true
2269                                }
2270                                _ => true,
2271                            };
2272                            buffers = value;
2273                        } else if opt.eq_ignore_ascii_case("timing") {
2274                            // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2275                            // the measured wall-clock annotation.
2276                            let value = match self.peek().clone() {
2277                                Token::On => {
2278                                    self.advance();
2279                                    true
2280                                }
2281                                Token::Ident(v) | Token::QuotedIdent(v)
2282                                    if v.eq_ignore_ascii_case("off") =>
2283                                {
2284                                    self.advance();
2285                                    false
2286                                }
2287                                Token::Ident(v) | Token::QuotedIdent(v)
2288                                    if v.eq_ignore_ascii_case("true") =>
2289                                {
2290                                    self.advance();
2291                                    true
2292                                }
2293                                _ => true,
2294                            };
2295                            timing_off = !value;
2296                        } else if opt.eq_ignore_ascii_case("settings") {
2297                            settings = true;
2298                        } else if opt.eq_ignore_ascii_case("wal") {
2299                            wal = true;
2300                        } else if opt.eq_ignore_ascii_case("summary") {
2301                            // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2302                            // gates the trailing Planning/Execution Time
2303                            // lines now (was accept-and-no-op).
2304                            let value = match self.peek().clone() {
2305                                Token::On => {
2306                                    self.advance();
2307                                    true
2308                                }
2309                                Token::Ident(v) | Token::QuotedIdent(v)
2310                                    if v.eq_ignore_ascii_case("off") =>
2311                                {
2312                                    self.advance();
2313                                    false
2314                                }
2315                                Token::Ident(v) | Token::QuotedIdent(v)
2316                                    if v.eq_ignore_ascii_case("true") =>
2317                                {
2318                                    self.advance();
2319                                    true
2320                                }
2321                                _ => true,
2322                            };
2323                            summary_off = !value;
2324                        } else if opt.eq_ignore_ascii_case("verbose")
2325                            || opt.eq_ignore_ascii_case("format")
2326                        {
2327                            // v7.37.22 — accept-but-no-op the remaining
2328                            // PG options so EXPLAIN-using clients
2329                            // (pgAdmin / DataGrip) don't see syntax
2330                            // errors. FORMAT takes a value (text /
2331                            // json / yaml / xml); skip the next token
2332                            // if it's an ident.
2333                            if opt.eq_ignore_ascii_case("format") {
2334                                if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2335                                {
2336                                    self.advance();
2337                                    format = match v.to_ascii_lowercase().as_str() {
2338                                        "text" => crate::ast::ExplainFormat::Text,
2339                                        "json" => crate::ast::ExplainFormat::Json,
2340                                        "xml" => crate::ast::ExplainFormat::Xml,
2341                                        "yaml" => crate::ast::ExplainFormat::Yaml,
2342                                        other => {
2343                                            return Err(self.err(format!(
2344                                                "EXPLAIN (FORMAT …): unknown format {other:?}; \
2345                                                 supports text, json, xml, yaml"
2346                                            )));
2347                                        }
2348                                    };
2349                                }
2350                            } else {
2351                                // VERBOSE / SUMMARY take optional ON/OFF;
2352                                // consume if present.
2353                                if matches!(self.peek(), Token::On) {
2354                                    self.advance();
2355                                } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2356                                    self.peek().clone()
2357                                    && (v.eq_ignore_ascii_case("off")
2358                                        || v.eq_ignore_ascii_case("true"))
2359                                {
2360                                    self.advance();
2361                                    let _ = v;
2362                                }
2363                            }
2364                        } else {
2365                            return Err(self.err(format!(
2366                                "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2367                            )));
2368                        }
2369                        if matches!(self.peek(), Token::Comma) {
2370                            self.advance();
2371                            continue;
2372                        }
2373                        break;
2374                    }
2375                    if !matches!(self.peek(), Token::RParen) {
2376                        return Err(self.err(format!(
2377                            "expected ')' after EXPLAIN options, got {:?}",
2378                            self.peek()
2379                        )));
2380                    }
2381                    self.advance();
2382                } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2383                    && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2384                {
2385                    self.advance();
2386                    analyze = true;
2387                }
2388                // v7.39 (round 224) — the body may open with WITH (CTEs);
2389                // route through the same CTE-then-SELECT path the top-level
2390                // WITH statement uses. v7.39 (round 225) — DML bodies parse
2391                // too (PG explains INSERT / UPDATE / DELETE).
2392                let inner = match self.peek().clone() {
2393                    Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2394                        self.advance();
2395                        self.parse_with_cte_then_select()?
2396                    }
2397                    Token::Insert => self.parse_insert_stmt(false)?,
2398                    Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2399                        self.advance();
2400                        self.parse_update_after_keyword()?
2401                    }
2402                    Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2403                        self.advance();
2404                        self.parse_delete_after_keyword()?
2405                    }
2406                    _ => self.parse_select_stmt()?,
2407                };
2408                if !matches!(
2409                    inner,
2410                    Statement::Select(_)
2411                        | Statement::Insert(_)
2412                        | Statement::Update(_)
2413                        | Statement::Delete(_)
2414                ) {
2415                    return Err(self.err(format!(
2416                        "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2417                    )));
2418                }
2419                Ok(Statement::Explain(crate::ast::ExplainStatement {
2420                    analyze,
2421                    inner: Box::new(inner),
2422                    suggest,
2423                    costs_off,
2424                    buffers,
2425                    timing_off,
2426                    settings,
2427                    wal,
2428                    summary_off,
2429                    format,
2430                }))
2431            }
2432            Token::Create => self.parse_create_stmt(),
2433            Token::Insert => self.parse_insert_stmt(false),
2434            // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2435            // spelling; route to the same handler. DESC is the
2436            // reserved ORDER BY token, so it gets its own arm.
2437            Token::Ident(s)
2438                if s.eq_ignore_ascii_case("describe")
2439                    && matches!(
2440                        self.tokens.get(self.pos + 1),
2441                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2442                    ) =>
2443            {
2444                self.advance();
2445                let table = self.expect_ident_like()?;
2446                Ok(Statement::ShowColumns(table))
2447            }
2448            Token::Desc
2449                if matches!(
2450                    self.tokens.get(self.pos + 1),
2451                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2452                ) =>
2453            {
2454                self.advance();
2455                let table = self.expect_ident_like()?;
2456                Ok(Statement::ShowColumns(table))
2457            }
2458            // `COPY table [(cols)] TO STDOUT` — the export half of
2459            // pg_dump's COPY pair (the FROM stdin half rides the
2460            // embed import path). Options need a format design and
2461            // error honestly.
2462            Token::Ident(s)
2463                if s.eq_ignore_ascii_case("copy")
2464                    && matches!(
2465                        self.tokens.get(self.pos + 1),
2466                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2467                    ) =>
2468            {
2469                self.advance(); // COPY
2470                let table = self.expect_ident_like()?;
2471                let columns = if matches!(self.peek(), Token::LParen) {
2472                    self.advance();
2473                    let mut cols = alloc::vec![self.expect_ident_like()?];
2474                    while matches!(self.peek(), Token::Comma) {
2475                        self.advance();
2476                        cols.push(self.expect_ident_like()?);
2477                    }
2478                    if !matches!(self.peek(), Token::RParen) {
2479                        return Err(self.err(format!(
2480                            "expected ')' after COPY column list, got {:?}",
2481                            self.peek()
2482                        )));
2483                    }
2484                    self.advance();
2485                    Some(cols)
2486                } else {
2487                    None
2488                };
2489                // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2490                // endpoint. (FROM STDIN still rides the wire/import path —
2491                // its data arrives out of band.)
2492                if matches!(self.peek(), Token::From)
2493                    && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2494                {
2495                    self.advance(); // FROM
2496                    let Token::String(path) = self.advance() else {
2497                        unreachable!()
2498                    };
2499                    let options = self.parse_copy_to_options()?;
2500                    return Ok(Statement::CopyFromFile {
2501                        table,
2502                        columns,
2503                        path,
2504                        options,
2505                    });
2506                }
2507                if !matches!(self.peek(), Token::To) {
2508                    return Err(self.err(format!(
2509                        "COPY: only TO STDOUT is supported here (FROM stdin \
2510                         rides the import path); got {:?}",
2511                        self.peek()
2512                    )));
2513                }
2514                self.advance();
2515                if matches!(self.peek(), Token::String(_)) {
2516                    let Token::String(path) = self.advance() else { unreachable!() };
2517                    let options = self.parse_copy_to_options()?;
2518                    return Ok(Statement::CopyToFile {
2519                        table,
2520                        columns,
2521                        query: None,
2522                        path,
2523                        options,
2524                    });
2525                }
2526                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2527                    return Err(self.err(format!(
2528                        "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2529                        self.peek()
2530                    )));
2531                }
2532                self.advance();
2533                let options = self.parse_copy_to_options()?;
2534                Ok(Statement::CopyTo {
2535                    table,
2536                    columns,
2537                    query: None,
2538                    options,
2539                })
2540            }
2541            // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2542            // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2543            // result set is streamed in COPY format (PG's query form).
2544            Token::Ident(s)
2545                if s.eq_ignore_ascii_case("copy")
2546                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2547            {
2548                self.advance(); // COPY
2549                self.advance(); // (
2550                let query = self.parse_select_stmt()?;
2551                if !matches!(self.peek(), Token::RParen) {
2552                    return Err(self.err(format!(
2553                        "expected ')' after COPY query, got {:?}",
2554                        self.peek()
2555                    )));
2556                }
2557                self.advance(); // )
2558                if !matches!(self.peek(), Token::To) {
2559                    return Err(self.err(format!(
2560                        "COPY (query): only TO STDOUT is supported, got {:?}",
2561                        self.peek()
2562                    )));
2563                }
2564                self.advance();
2565                if matches!(self.peek(), Token::String(_)) {
2566                    let Token::String(path) = self.advance() else { unreachable!() };
2567                    let options = self.parse_copy_to_options()?;
2568                    return Ok(Statement::CopyToFile {
2569                        table: String::new(),
2570                        columns: None,
2571                        query: Some(alloc::boxed::Box::new(query)),
2572                        path,
2573                        options,
2574                    });
2575                }
2576                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2577                    return Err(self.err(format!(
2578                        "COPY (query): TO supports STDOUT only, got {:?}",
2579                        self.peek()
2580                    )));
2581                }
2582                self.advance();
2583                let options = self.parse_copy_to_options()?;
2584                Ok(Statement::CopyTo {
2585                    table: String::new(),
2586                    columns: None,
2587                    query: Some(alloc::boxed::Box::new(query)),
2588                    options,
2589                })
2590            }
2591            // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2592            // Shares the INSERT body; the replace flag lowers it
2593            // onto ON CONFLICT DO UPDATE with an empty assignment
2594            // list (engine: replace the whole row).
2595            Token::Ident(s)
2596                if s.eq_ignore_ascii_case("replace")
2597                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2598            {
2599                self.parse_insert_stmt(true)
2600            }
2601            Token::Begin => {
2602                self.advance();
2603                // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2604                // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2605                // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2606                // is consumed first, then the trailing modes — including the
2607                // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2608                // WORK/TRANSACTION). The explicit level, when present, rides the
2609                // statement so `exec_begin` applies it for this transaction.
2610                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2611                {
2612                    self.advance();
2613                }
2614                let iso = self.parse_isolation_level_clauses()?;
2615                Ok(Statement::Begin(iso))
2616            }
2617            // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2618            // for BEGIN. START is contextual in PG too; pattern-match
2619            // on the ident here. Iso clauses are parse-and-ignored,
2620            // same as BEGIN above.
2621            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2622                self.advance();
2623                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2624                {
2625                    return Err(self.err(alloc::format!(
2626                        "expected TRANSACTION after START, got {:?}",
2627                        self.peek()
2628                    )));
2629                }
2630                self.advance();
2631                let iso = self.parse_isolation_level_clauses()?;
2632                Ok(Statement::Begin(iso))
2633            }
2634            Token::Commit => {
2635                self.advance();
2636                Ok(Statement::Commit)
2637            }
2638            Token::Rollback => {
2639                self.advance();
2640                // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2641                // savepoint without ending the transaction. Bare
2642                // `ROLLBACK` drops the whole TX.
2643                if matches!(self.peek(), Token::To) {
2644                    self.advance();
2645                    if matches!(self.peek(), Token::Savepoint) {
2646                        self.advance();
2647                    }
2648                    let name = self.expect_ident_like()?;
2649                    Ok(Statement::RollbackToSavepoint(name))
2650                } else {
2651                    Ok(Statement::Rollback)
2652                }
2653            }
2654            Token::Savepoint => {
2655                self.advance();
2656                let name = self.expect_ident_like()?;
2657                Ok(Statement::Savepoint(name))
2658            }
2659            Token::Release => {
2660                self.advance();
2661                // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2662                // is optional in standard SQL.
2663                if matches!(self.peek(), Token::Savepoint) {
2664                    self.advance();
2665                }
2666                let name = self.expect_ident_like()?;
2667                Ok(Statement::ReleaseSavepoint(name))
2668            }
2669            Token::Show => {
2670                self.advance();
2671                // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2672                // v6.1.2 promoted TABLES to a reserved keyword (for
2673                // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2674                // arrives as `Token::Tables` rather than a bare ident.
2675                // USERS / COLUMNS remain bare idents.
2676                let target = match self.advance() {
2677                    Token::Tables => "tables".to_string(),
2678                    // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2679                    // keyword token; recognise it as the SHOW CREATE
2680                    // dispatch keyword too.
2681                    Token::Create => "create".to_string(),
2682                    // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2683                    // keyword too; let SHOW INDEX FROM parse.
2684                    Token::Index => "index".to_string(),
2685                    // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2686                    // reserved (used in aggregate function calls);
2687                    // recognise it here so the parser dispatches
2688                    // to ShowParameter("all") — the engine returns
2689                    // the curated parameter inventory.
2690                    Token::All => "all".to_string(),
2691                    Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2692                    other => {
2693                        return Err(self.err(format!(
2694                            "expected SHOW target, got {other:?}"
2695                        )));
2696                    }
2697                };
2698                match target.as_str() {
2699                    "tables" => Ok(Statement::ShowTables),
2700                    "users" => Ok(Statement::ShowUsers),
2701                    // v7.38 轴 4 — `SHOW transaction_isolation`
2702                    // returns the currently-selected isolation level.
2703                    "transaction_isolation" => Ok(Statement::ShowParameter(
2704                        "transaction_isolation".to_string(),
2705                    )),
2706                    // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2707                    // TABLE <t>` returns a 2-column row: (Table,
2708                    // Create Table). mysqldump emits this for every
2709                    // table at scrape time; without it the dump
2710                    // round-trip stalls.
2711                    // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2712                    // FROM <t>` (also spelled `SHOW INDEX` and
2713                    // `SHOW KEYS`). admin / mysqldump probes use
2714                    // it to list per-table indexes.
2715                    "indexes" | "index" | "keys" => {
2716                        if !matches!(self.peek(), Token::From) {
2717                            return Err(self.err(format!(
2718                                "expected FROM after SHOW INDEXES, got {:?}",
2719                                self.peek()
2720                            )));
2721                        }
2722                        self.advance();
2723                        let table = self.expect_ident_like()?;
2724                        Ok(Statement::ShowIndexes(table))
2725                    }
2726                    // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2727                    // `SHOW VARIABLES`. Both return a 2-column row
2728                    // set listing server-side state; clients probe
2729                    // them at connect time.
2730                    "status" => Ok(Statement::ShowStatus),
2731                    "variables" => Ok(Statement::ShowVariables),
2732                    // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2733                    "processlist" => Ok(Statement::ShowProcesslist),
2734                    "create" => {
2735                        // SHOW CREATE TABLE / VIEW / DATABASE — only
2736                        // TABLE is supported in v7.17.
2737                        let kind = match self.advance() {
2738                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2739                            Token::Table => "table".to_string(),
2740                            other => {
2741                                return Err(self.err(format!(
2742                                    "expected TABLE after SHOW CREATE, got {other:?}"
2743                                )));
2744                            }
2745                        };
2746                        if !kind.eq_ignore_ascii_case("table") {
2747                            return Err(self.err(format!(
2748                                "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2749                            )));
2750                        }
2751                        let name = self.expect_ident_like()?;
2752                        Ok(Statement::ShowCreateTable(name))
2753                    }
2754                    // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2755                    // (and `SHOW SCHEMAS` alias). The mysql client uses
2756                    // it to populate the database selector at connect
2757                    // time; without it `mysql -p` errors before the
2758                    // first user query.
2759                    "databases" | "schemas" => Ok(Statement::ShowDatabases),
2760                    // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2761                    // keyword on its own; it lands here as a bare
2762                    // ident. Returning all publications + their
2763                    // scope summary.
2764                    "publications" => Ok(Statement::ShowPublications),
2765                    // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2766                    "subscriptions" => Ok(Statement::ShowSubscriptions),
2767                    "columns" => {
2768                        if !matches!(self.peek(), Token::From) {
2769                            return Err(self.err(format!(
2770                                "expected FROM after SHOW COLUMNS, got {:?}",
2771                                self.peek()
2772                            )));
2773                        }
2774                        self.advance();
2775                        let table = self.expect_ident_like()?;
2776                        Ok(Statement::ShowColumns(table))
2777                    }
2778                    // v7.38 轴 4 surface — `SHOW <param>` for any
2779                    // remaining session / preset parameter name
2780                    // (server_version, search_path, client_encoding,
2781                    // …). The engine's ShowParameter handler does the
2782                    // dispatch; unrecognised names error there with
2783                    // a pointer to pg_settings, not at parse time —
2784                    // so a driver that issues `SHOW spam_setting`
2785                    // gets a clear runtime error instead of a
2786                    // confusing "unknown SHOW target".
2787                    other => {
2788                        // v7.38 (read01 P3.20) — a custom namespaced GUC
2789                        // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2790                        // consume the dotted tail so it round-trips with
2791                        // `SET app.foo` / `current_setting('app.foo')`.
2792                        let mut full = other.to_string();
2793                        while matches!(self.peek(), Token::Dot) {
2794                            self.advance();
2795                            let seg = self.expect_ident_like()?;
2796                            full.push('.');
2797                            full.push_str(&seg.to_ascii_lowercase());
2798                        }
2799                        Ok(Statement::ShowParameter(full))
2800                    }
2801                }
2802            }
2803            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2804            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2805            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2806            // arrived as a bare ident; tokenising it dedicatedly
2807            // keeps the dispatch tree small.
2808            Token::Drop => {
2809                self.advance();
2810                match self.peek() {
2811                    // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2812                    // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2813                    // around DROP ROLE cleanup. SPG has no role-owner
2814                    // model, so consume to boundary as a no-op.
2815                    Token::Ident(s) | Token::QuotedIdent(s)
2816                        if s.eq_ignore_ascii_case("owned") =>
2817                    {
2818                        // v7.39 (round 696) — still a no-op (SPG has no
2819                        // role-owner model), but the ROLE is carried out so
2820                        // the engine can refuse one that does not exist,
2821                        // which is what PG18 does.
2822                        self.advance();
2823                        if self.peek_is_by() {
2824                            self.advance();
2825                        }
2826                        let names = self.take_comma_separated_names();
2827                        self.consume_until_statement_boundary();
2828                        Ok(Statement::ValidateOnly {
2829                            kind: crate::ast::ValidateOnlyKind::RoleName,
2830                            names,
2831                        })
2832                    }
2833                    // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
2834                    // It drops only a TEMPORARY table, and name resolution
2835                    // already prefers the session's own, so the keyword is
2836                    // consumed and the ordinary DROP TABLE path runs.
2837                    Token::Ident(s) | Token::QuotedIdent(s)
2838                        if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
2839                    {
2840                        self.advance();
2841                        if !matches!(self.peek(), Token::Table) {
2842                            return Err(self.err(alloc::format!(
2843                                "expected TABLE after DROP TEMPORARY, got {:?}",
2844                                self.peek()
2845                            )));
2846                        }
2847                        self.parse_drop_table_after_keyword()
2848                    }
2849                    Token::Publication => {
2850                        self.advance();
2851                        // v7.39 (round 754, F31-B4) — the round-753
2852                        // audit probe tripped over the missing
2853                        // `IF EXISTS` here (syntax error).
2854                        let if_exists = self.consume_if_exists();
2855                        let name = self.expect_ident_or_string()?;
2856                        Ok(Statement::DropPublication { name, if_exists })
2857                    }
2858                    Token::Subscription => {
2859                        self.advance();
2860                        let if_exists = self.consume_if_exists();
2861                        let name = self.expect_ident_or_string()?;
2862                        Ok(Statement::DropSubscription { name, if_exists })
2863                    }
2864                    Token::Ident(s) | Token::QuotedIdent(s)
2865                        if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
2866                    {
2867                        self.advance();
2868                        // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
2869                        // login user IS a role in PG, and SPG's store holds
2870                        // both. `IF EXISTS` is accepted on either spelling.
2871                        let if_exists = self.consume_if_exists();
2872                        let name = self.expect_ident_or_string()?;
2873                        Ok(Statement::DropUser { name, if_exists })
2874                    }
2875                    // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
2876                    // CREATE DATABASE has parsed since v7.14 and this did
2877                    // not, so `DROP DATABASE IF EXISTS x` — what every
2878                    // teardown script and pg_dumpall preamble opens with —
2879                    // came back as a syntax error, which IF EXISTS cannot
2880                    // soften. The name is carried so the engine can answer
2881                    // the way PG does; PG never lets this succeed on a
2882                    // single-database server, since the name is either
2883                    // unknown ("database … does not exist", or a notice
2884                    // under IF EXISTS) or the one you are connected to
2885                    // ("cannot drop the currently open database").
2886                    Token::Ident(s) | Token::QuotedIdent(s)
2887                        if s.eq_ignore_ascii_case("database") =>
2888                    {
2889                        self.advance();
2890                        let if_exists = self.consume_if_exists();
2891                        let name = self.expect_ident_or_string()?;
2892                        self.consume_until_statement_boundary();
2893                        Ok(Statement::DropDatabase { name, if_exists })
2894                    }
2895                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
2896                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
2897                        self.advance();
2898                        let if_exists = self.consume_if_exists();
2899                        let name = self.expect_ident_like()?;
2900                        // ON <table>
2901                        if !matches!(self.peek(), Token::On) {
2902                            return Err(self.err(alloc::format!(
2903                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
2904                                self.peek()
2905                            )));
2906                        }
2907                        self.advance();
2908                        let table = self.expect_ident_like()?;
2909                        Ok(Statement::DropTrigger {
2910                            name,
2911                            table,
2912                            if_exists,
2913                        })
2914                    }
2915                    // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
2916                    // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
2917                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
2918                        self.advance();
2919                        let if_exists = self.consume_if_exists();
2920                        let name = self.expect_ident_like()?;
2921                        if !matches!(self.peek(), Token::On) {
2922                            return Err(self.err(alloc::format!(
2923                                "expected ON <table> after DROP RULE {name:?}, got {:?}",
2924                                self.peek()
2925                            )));
2926                        }
2927                        self.advance();
2928                        let table = self.expect_ident_like()?;
2929                        // Optional CASCADE / RESTRICT — accepted, no effect.
2930                        self.consume_until_statement_boundary();
2931                        Ok(Statement::DropRule {
2932                            name,
2933                            table,
2934                            if_exists,
2935                        })
2936                    }
2937                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
2938                    // v7.12.4 ignores any optional arg-list (signature-
2939                    // based overload disambiguation lands in v7.12.5+).
2940                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
2941                        self.advance();
2942                        let if_exists = self.consume_if_exists();
2943                        let name = self.expect_ident_like()?;
2944                        // v7.39 (read01 round 62) — the argument list identifies
2945                        // WHICH overload to drop, so it is captured, not
2946                        // discarded. `DROP FUNCTION f` (no list) is legal when
2947                        // the name is unambiguous; the engine enforces that.
2948                        let args = if matches!(self.peek(), Token::LParen) {
2949                            Some(self.parse_function_signature_types()?)
2950                        } else {
2951                            None
2952                        };
2953                        // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
2954                        // trailer, which `DROP TABLE` and `DROP INDEX` have
2955                        // accepted since v7.14 and this one refused outright.
2956                        // pg_dump writes it, so refusing was a parse error in
2957                        // the middle of a restore. SPG drops the function
2958                        // either way — it tracks no dependents to cascade to —
2959                        // which is the same reading the other two give it.
2960                        self.consume_drop_behaviour();
2961                        Ok(Statement::DropFunction {
2962                            name,
2963                            args,
2964                            if_exists,
2965                        })
2966                    }
2967                    // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
2968                    // [CASCADE|RESTRICT]. pg_dump and mysqldump both
2969                    // emit DROP TABLE IF EXISTS at the head of every
2970                    // CREATE TABLE block so re-importing a dump
2971                    // overwrites prior state. SPG accepts and removes
2972                    // matching tables; CASCADE/RESTRICT trailers
2973                    // accepted silently.
2974                    Token::Table => self.parse_drop_table_after_keyword(),
2975                    // v7.14.0 — DROP INDEX [IF EXISTS] name
2976                    // [CASCADE|RESTRICT]. PG / mysqldump emit this
2977                    // for partial-index renames and pgvector
2978                    // migrations. SPG removes the matching index;
2979                    // IF EXISTS makes the drop idempotent.
2980                    Token::Index => {
2981                        self.advance();
2982                        let if_exists = self.consume_if_exists();
2983                        let name = self.expect_ident_like()?;
2984                        if matches!(
2985                            self.peek(),
2986                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
2987                                || s.eq_ignore_ascii_case("restrict")
2988                        ) {
2989                            self.advance();
2990                        }
2991                        Ok(Statement::DropIndex { name, if_exists })
2992                    }
2993                    // v7.14.0 — DROP SCHEMA [IF EXISTS] name
2994                    // [CASCADE|RESTRICT]. SPG is single-database;
2995                    // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
2996                    // name [, name…] [CASCADE | RESTRICT]. Real
2997                    // unregister (was silent no-op pre-v7.17).
2998                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
2999                        self.advance();
3000                        let if_exists = self.consume_if_exists();
3001                        let mut names = vec![self.expect_ident_like()?];
3002                        while matches!(self.peek(), Token::Comma) {
3003                            self.advance();
3004                            names.push(self.expect_ident_like()?);
3005                        }
3006                        if matches!(
3007                            self.peek(),
3008                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3009                                || s.eq_ignore_ascii_case("restrict")
3010                        ) {
3011                            self.advance();
3012                        }
3013                        Ok(Statement::DropSchema { names, if_exists })
3014                    }
3015                    // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3016                    // name [, name…] [CASCADE|RESTRICT].
3017                    Token::Ident(s) | Token::QuotedIdent(s)
3018                        if s.eq_ignore_ascii_case("type") =>
3019                    {
3020                        self.advance();
3021                        let if_exists = self.consume_if_exists();
3022                        let mut names = vec![self.expect_ident_like()?];
3023                        while matches!(self.peek(), Token::Comma) {
3024                            self.advance();
3025                            names.push(self.expect_ident_like()?);
3026                        }
3027                        if matches!(
3028                            self.peek(),
3029                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3030                                || s.eq_ignore_ascii_case("restrict")
3031                        ) {
3032                            self.advance();
3033                        }
3034                        Ok(Statement::DropType { names, if_exists })
3035                    }
3036                    // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3037                    // name [, name…] [CASCADE|RESTRICT].
3038                    Token::Ident(s) | Token::QuotedIdent(s)
3039                        if s.eq_ignore_ascii_case("domain") =>
3040                    {
3041                        self.advance();
3042                        let if_exists = self.consume_if_exists();
3043                        let mut names = vec![self.expect_ident_like()?];
3044                        while matches!(self.peek(), Token::Comma) {
3045                            self.advance();
3046                            names.push(self.expect_ident_like()?);
3047                        }
3048                        if matches!(
3049                            self.peek(),
3050                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3051                                || s.eq_ignore_ascii_case("restrict")
3052                        ) {
3053                            self.advance();
3054                        }
3055                        Ok(Statement::DropDomain { names, if_exists })
3056                    }
3057                    // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3058                    // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3059                    Token::Ident(s) | Token::QuotedIdent(s)
3060                        if s.eq_ignore_ascii_case("materialized") =>
3061                    {
3062                        self.advance();
3063                        let nxt = self.peek().clone();
3064                        if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3065                        {
3066                            return Err(self.err(alloc::format!(
3067                                "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3068                            )));
3069                        }
3070                        self.advance();
3071                        let if_exists = self.consume_if_exists();
3072                        let mut names = vec![self.expect_ident_like()?];
3073                        while matches!(self.peek(), Token::Comma) {
3074                            self.advance();
3075                            names.push(self.expect_ident_like()?);
3076                        }
3077                        if matches!(
3078                            self.peek(),
3079                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3080                                || s.eq_ignore_ascii_case("restrict")
3081                        ) {
3082                            self.advance();
3083                        }
3084                        Ok(Statement::DropMaterializedView { names, if_exists })
3085                    }
3086                    // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3087                    // name [, name…] [CASCADE|RESTRICT].
3088                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3089                        self.advance();
3090                        let if_exists = self.consume_if_exists();
3091                        let mut names = vec![self.expect_ident_like()?];
3092                        while matches!(self.peek(), Token::Comma) {
3093                            self.advance();
3094                            names.push(self.expect_ident_like()?);
3095                        }
3096                        if matches!(
3097                            self.peek(),
3098                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3099                                || s.eq_ignore_ascii_case("restrict")
3100                        ) {
3101                            self.advance();
3102                        }
3103                        Ok(Statement::DropView { names, if_exists })
3104                    }
3105                    // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3106                    // [CASCADE|RESTRICT]. Real removal from catalog
3107                    // (was a silent no-op pre-v7.17).
3108                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3109                        self.advance();
3110                        let if_exists = self.consume_if_exists();
3111                        let mut names = vec![self.expect_ident_like()?];
3112                        while matches!(self.peek(), Token::Comma) {
3113                            self.advance();
3114                            names.push(self.expect_ident_like()?);
3115                        }
3116                        if matches!(
3117                            self.peek(),
3118                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3119                                || s.eq_ignore_ascii_case("restrict")
3120                        ) {
3121                            self.advance();
3122                        }
3123                        Ok(Statement::DropSequence { names, if_exists })
3124                    }
3125                    // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3126                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3127                        self.advance();
3128                        self.parse_drop_policy_after_keyword()
3129                    }
3130                    // v7.37.17 (17.6 siblings) — DROP <target> for
3131                    // targets SPG doesn't natively track. pg_dump
3132                    // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3133                    // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3134                    // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3135                    // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3136                    // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3137                    // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3138                    // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3139                    // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3140                    // etc. — accept + Empty-return so pg_dump tails
3141                    // load through. Materialized-view drop dispatches
3142                    // to the existing DropTable path when the token
3143                    // is Materialized-View-shaped (elsewhere in
3144                    // this parser).
3145                    Token::Ident(s) | Token::QuotedIdent(s)
3146                        if s.eq_ignore_ascii_case("text")
3147                            // The DROP dispatch matches on PEEK — `text` is
3148                            // not yet consumed, so SEARCH/CONFIGURATION sit
3149                            // at pos+1/pos+2 (the round-695 trap's mirror).
3150                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3151                            && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3152                    {
3153                        // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3154                        // validates the name; DICTIONARY / PARSER / TEMPLATE
3155                        // stay in the noise arm below.
3156                        self.advance(); // TEXT
3157                        self.advance(); // SEARCH
3158                        self.advance(); // CONFIGURATION
3159                        let if_exists = self.consume_if_exists();
3160                        let names = self.take_comma_separated_names();
3161                        self.consume_until_statement_boundary();
3162                        if if_exists {
3163                            return Ok(Statement::Empty);
3164                        }
3165                        Ok(Statement::ValidateOnly {
3166                            kind: crate::ast::ValidateOnlyKind::TsConfigName,
3167                            names,
3168                        })
3169                    }
3170                    Token::Ident(s) | Token::QuotedIdent(s)
3171                        if matches!(
3172                            s.to_ascii_lowercase().as_str(),
3173                            "type"
3174                                | "domain"
3175                                | "operator"
3176                                | "cast"
3177                                // `text` = TEXT SEARCH DICTIONARY / PARSER /
3178                                // TEMPLATE (CONFIGURATION intercepted above).
3179                                | "text"
3180                                | "materialized"
3181                                | "large"
3182                                | "role"
3183                                | "access"
3184                                | "procedure"
3185                                | "routine"
3186                        ) =>
3187                    {
3188                        self.consume_until_statement_boundary();
3189                        Ok(Statement::Empty)
3190                    }
3191                    // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3192                    // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3193                    // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3194                    // foreign-data warning family (round 706) so a
3195                    // CREATE→DROP sequence in a dump stays consistent.
3196                    Token::Ident(s) | Token::QuotedIdent(s)
3197                        if s.eq_ignore_ascii_case("server")
3198                            || s.eq_ignore_ascii_case("foreign") =>
3199                    {
3200                        self.advance();
3201                        self.consume_until_statement_boundary();
3202                        Ok(Statement::ValidateOnly {
3203                            kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3204                            names: Vec::new(),
3205                        })
3206                    }
3207                    Token::Ident(s) | Token::QuotedIdent(s)
3208                        if s.eq_ignore_ascii_case("collation")
3209                            || s.eq_ignore_ascii_case("tablespace") =>
3210                    {
3211                        let kind = if s.eq_ignore_ascii_case("collation") {
3212                            crate::ast::ValidateOnlyKind::CollationName
3213                        } else {
3214                            crate::ast::ValidateOnlyKind::TablespaceName
3215                        };
3216                        self.advance();
3217                        let if_exists = self.consume_if_exists();
3218                        let names = self.take_comma_separated_names();
3219                        self.consume_until_statement_boundary();
3220                        if if_exists {
3221                            return Ok(Statement::Empty);
3222                        }
3223                        Ok(Statement::ValidateOnly { kind, names })
3224                    }
3225                    Token::Ident(s) | Token::QuotedIdent(s)
3226                        if s.eq_ignore_ascii_case("event") =>
3227                    {
3228                        self.advance();
3229                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3230                        {
3231                            self.advance();
3232                        }
3233                        let if_exists = self.consume_if_exists();
3234                        let names = self.take_comma_separated_names();
3235                        self.consume_until_statement_boundary();
3236                        if if_exists {
3237                            return Ok(Statement::Empty);
3238                        }
3239                        Ok(Statement::ValidateOnly {
3240                            kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3241                            names,
3242                        })
3243                    }
3244                    // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3245                    // leave the noise list; see the ValidateOnly kinds.
3246                    Token::Ident(s) | Token::QuotedIdent(s)
3247                        if s.eq_ignore_ascii_case("conversion")
3248                            || s.eq_ignore_ascii_case("language")
3249                            // `DROP PROCEDURAL LANGUAGE` puts the modifier
3250                            // FIRST — the first draft looked for it after.
3251                            || s.eq_ignore_ascii_case("procedural") =>
3252                    {
3253                        let kind = if s.eq_ignore_ascii_case("conversion") {
3254                            crate::ast::ValidateOnlyKind::ConversionName
3255                        } else {
3256                            crate::ast::ValidateOnlyKind::LanguageName
3257                        };
3258                        self.advance();
3259                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3260                        {
3261                            self.advance();
3262                        }
3263                        let if_exists = self.consume_if_exists();
3264                        let names = self.take_comma_separated_names();
3265                        self.consume_until_statement_boundary();
3266                        if if_exists {
3267                            return Ok(Statement::Empty);
3268                        }
3269                        Ok(Statement::ValidateOnly { kind, names })
3270                    }
3271                    // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3272                    // name(argtypes)[, …]`. Parsed for real so the engine
3273                    // can answer as PG does; see Statement::DropAggregate.
3274                    Token::Ident(s) | Token::QuotedIdent(s)
3275                        if s.eq_ignore_ascii_case("aggregate") =>
3276                    {
3277                        self.advance();
3278                        let if_exists = self.consume_if_exists();
3279                        let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3280                        loop {
3281                            let name = self.expect_ident_like()?;
3282                            if !matches!(self.peek(), Token::LParen) {
3283                                return Err(self.err(alloc::format!(
3284                                    "expected argument list after DROP AGGREGATE {name}"
3285                                )));
3286                            }
3287                            self.advance();
3288                            let mut args: Vec<String> = Vec::new();
3289                            let mut star = false;
3290                            loop {
3291                                match self.peek().clone() {
3292                                    Token::RParen => {
3293                                        self.advance();
3294                                        break;
3295                                    }
3296                                    Token::Star => {
3297                                        self.advance();
3298                                        star = true;
3299                                    }
3300                                    Token::Comma => {
3301                                        self.advance();
3302                                    }
3303                                    _ => {
3304                                        // A type name may be multi-token
3305                                        // (`double precision`); glue idents
3306                                        // until , or ).
3307                                        let mut t = self.expect_ident_like()?;
3308                                        while let Token::Ident(nx) = self.peek() {
3309                                            let nx = nx.clone();
3310                                            self.advance();
3311                                            t.push(' ');
3312                                            t.push_str(&nx);
3313                                        }
3314                                        args.push(t);
3315                                    }
3316                                }
3317                            }
3318                            items.push((name, if star { None } else { Some(args) }));
3319                            if matches!(self.peek(), Token::Comma) {
3320                                self.advance();
3321                            } else {
3322                                break;
3323                            }
3324                        }
3325                        self.consume_until_statement_boundary();
3326                        Ok(Statement::DropAggregate { if_exists, items })
3327                    }
3328                    // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3329                    // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3330                    // installed; `IF EXISTS` is the spelling that says do
3331                    // not, and it keeps the no-op.
3332                    Token::Ident(s) | Token::QuotedIdent(s)
3333                        if s.eq_ignore_ascii_case("extension") =>
3334                    {
3335                        self.advance();
3336                        let if_exists = self.consume_if_exists();
3337                        let names = self.take_comma_separated_names();
3338                        self.consume_until_statement_boundary();
3339                        if if_exists {
3340                            return Ok(Statement::Empty);
3341                        }
3342                        Ok(Statement::ValidateOnly {
3343                            kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3344                            names,
3345                        })
3346                    }
3347                    Token::Ident(s) | Token::QuotedIdent(s)
3348                        if s.eq_ignore_ascii_case("statistics") =>
3349                    {
3350                        self.parse_drop_statistics_after_drop()
3351                    }
3352                    other => Err(self.err(format!(
3353                        "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3354                         SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3355                    ))),
3356                }
3357            }
3358            // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3359            // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3360            // and accepted before the view name. SPG materialised
3361            // views re-evaluate on read (always-fresh semantics), so
3362            // the CONCURRENTLY-vs-serial distinction has no runtime
3363            // effect — the refresh body does not block readers either
3364            // way. Same accept-and-no-op pattern as DETACH PARTITION
3365            // CONCURRENTLY (16.5).
3366            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3367                self.advance();
3368                let nxt = self.peek().clone();
3369                if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3370                {
3371                    return Err(self.err(alloc::format!(
3372                        "expected MATERIALIZED after REFRESH, got {nxt:?}"
3373                    )));
3374                }
3375                self.advance();
3376                let nxt2 = self.peek().clone();
3377                if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3378                {
3379                    return Err(self.err(alloc::format!(
3380                        "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3381                    )));
3382                }
3383                self.advance();
3384                // Optional CONCURRENTLY noise word — consumed without
3385                // changing semantics.
3386                if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3387                {
3388                    self.advance();
3389                }
3390                let name = self.expect_ident_like()?;
3391                let with_data = self.parse_optional_with_data(true)?;
3392                Ok(Statement::RefreshMaterializedView { name, with_data })
3393            }
3394            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3395                self.advance();
3396                self.parse_update_after_keyword()
3397            }
3398            // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3399            // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3400            // [CASCADE | RESTRICT]. Clears every row from each named
3401            // table. Parses at the top level; the engine dispatcher
3402            // walks Statement::Truncate.
3403            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3404                self.advance();
3405                // Optional TABLE noise word — PG accepts both the reserved
3406                // token and the bare identifier spelling.
3407                if matches!(self.peek(), Token::Table)
3408                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3409                {
3410                    self.advance();
3411                }
3412                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3413                // not absorbed. The lookahead keeps a table genuinely
3414                // called `only` working: the keyword is a keyword only
3415                // when a name follows it.
3416                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3417                    if s.eq_ignore_ascii_case("only"))
3418                    && matches!(
3419                        self.tokens.get(self.pos + 1),
3420                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3421                    );
3422                if only {
3423                    self.advance();
3424                }
3425                // Table names (comma-separated).
3426                let mut tables = Vec::new();
3427                loop {
3428                    tables.push(self.expect_ident_like()?);
3429                    if matches!(self.peek(), Token::Comma) {
3430                        self.advance();
3431                        continue;
3432                    }
3433                    break;
3434                }
3435                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3436                let mut restart_identity = false;
3437                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3438                {
3439                    self.advance();
3440                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3441                    {
3442                        self.advance();
3443                        restart_identity = true;
3444                    }
3445                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3446                {
3447                    self.advance();
3448                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3449                    {
3450                        self.advance();
3451                    }
3452                }
3453                // Optional CASCADE / RESTRICT.
3454                let mut cascade = false;
3455                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3456                {
3457                    self.advance();
3458                    cascade = true;
3459                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3460                {
3461                    self.advance();
3462                }
3463                Ok(Statement::Truncate {
3464                    tables,
3465                    restart_identity,
3466                    cascade,
3467                    only,
3468                })
3469            }
3470            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3471            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3472            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3473            // rows change so the index tree is always up-to-date;
3474            // REINDEX is a strict no-op. Accept the whole statement
3475            // shape to boundary for pg_dump round-trip compatibility.
3476            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3477                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3478                // index bloat to rebuild, so the work stays a no-op, but PG
3479                // validates what it was pointed at and this swallowed the
3480                // name at parse time — `REINDEX TABLE typo` reported
3481                // success. Measured on PG18: INDEX / TABLE name a relation,
3482                // SCHEMA a schema, SYSTEM nothing.
3483                self.advance();
3484                self.parse_reindex_tail()
3485            }
3486            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3487            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3488            // SPG has no MVCC bloat today (Phase D visibility map
3489            // queues with v7.38); the freezer collapses hot-tier
3490            // rows into cold segments automatically. VACUUM is a
3491            // no-op — pg_dump maintenance scripts and Discourse's
3492            // periodic-maintenance path both emit it.
3493            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3494            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3495            // actual bloat, so the pre-MVCC accept-and-ignore posture
3496            // became a silent no-op on a customer's manual reclaim.
3497            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3498            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3499            // ANALYZE is captured, the optional table name is captured.
3500            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3501                self.advance();
3502                // Parenthesised option list: absorb it.
3503                if matches!(self.peek(), Token::LParen) {
3504                    let mut depth = 0usize;
3505                    loop {
3506                        match self.advance() {
3507                            Token::LParen => depth += 1,
3508                            Token::RParen => {
3509                                depth -= 1;
3510                                if depth == 0 {
3511                                    break;
3512                                }
3513                            }
3514                            Token::Eof => break,
3515                            _ => {}
3516                        }
3517                    }
3518                }
3519                let mut analyze = false;
3520                let mut table: Option<String> = None;
3521                loop {
3522                    match self.peek() {
3523                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3524                        // an identifier, so the loop below broke out on it and
3525                        // dropped the table name: `VACUUM FULL nosuch` was
3526                        // accepted where `VACUUM nosuch` was refused.
3527                        Token::Full => {
3528                            self.advance();
3529                        }
3530                        Token::Ident(w) | Token::QuotedIdent(w) => {
3531                            let wl = w.to_ascii_lowercase();
3532                            match wl.as_str() {
3533                                "full" | "freeze" | "verbose" => {
3534                                    self.advance();
3535                                }
3536                                "analyze" | "analyse" => {
3537                                    analyze = true;
3538                                    self.advance();
3539                                }
3540                                _ => {
3541                                    table = Some(self.expect_ident_like()?);
3542                                    break;
3543                                }
3544                            }
3545                        }
3546                        _ => break,
3547                    }
3548                }
3549                // Optional trailing column list / anything else to the
3550                // statement boundary (PG accepts per-column ANALYZE).
3551                self.consume_until_statement_boundary();
3552                Ok(Statement::Vacuum { table, analyze })
3553            }
3554            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3555            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3556            // <index>. PG stores rows in physical order matching
3557            // an index; SPG's hot-tier is append-only + cold-tier
3558            // is segment-frozen, so clustering has no persistent
3559            // effect. Accept-and-no-op for pg_dump compat.
3560            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3561                // v7.39 (round 535) — same as REINDEX above: the relation is
3562                // carried so the engine can refuse one that does not exist.
3563                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3564                self.advance();
3565                self.parse_cluster_tail()
3566            }
3567            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3568            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3569            // optional string payload; UNLISTEN takes a channel or `*`.
3570            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3571                self.advance();
3572                let ch = match self.advance() {
3573                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3574                    other => {
3575                        return Err(self.err(format!(
3576                            "expected channel name after LISTEN, got {other:?}"
3577                        )));
3578                    }
3579                };
3580                Ok(Statement::Listen(ch))
3581            }
3582            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3583                self.advance();
3584                let channel = match self.advance() {
3585                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3586                    other => {
3587                        return Err(self.err(format!(
3588                            "expected channel name after NOTIFY, got {other:?}"
3589                        )));
3590                    }
3591                };
3592                let payload = if matches!(self.peek(), Token::Comma) {
3593                    self.advance();
3594                    match self.advance() {
3595                        Token::String(p) => Some(p),
3596                        other => {
3597                            return Err(self.err(format!(
3598                                "expected string payload after NOTIFY <channel>, got {other:?}"
3599                            )));
3600                        }
3601                    }
3602                } else {
3603                    None
3604                };
3605                Ok(Statement::Notify { channel, payload })
3606            }
3607            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3608                self.advance();
3609                match self.advance() {
3610                    Token::Star => Ok(Statement::Unlisten(None)),
3611                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3612                    other => Err(self.err(format!(
3613                        "expected channel name or * after UNLISTEN, got {other:?}"
3614                    ))),
3615                }
3616            }
3617            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3618            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3619            // process-wide write lock today; explicit LOCK has no
3620            // effect. Accept-and-no-op for pg_dump / migration
3621            // compat.
3622            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3623                self.advance();
3624                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3625                // engine holds a process-wide write lock), but the TABLE
3626                // NAME is now carried out so the engine can refuse one that
3627                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3628                // READ|WRITE` is a different statement with the same first
3629                // word; it keeps the old no-op, because a MySQL dump's
3630                // bracket names tables it is about to create.
3631                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3632                    if k.eq_ignore_ascii_case("tables"));
3633                if mysql_tables {
3634                    self.consume_until_statement_boundary();
3635                    return Ok(Statement::Empty);
3636                }
3637                if matches!(self.peek(), Token::Table) {
3638                    self.advance();
3639                }
3640                let names = self.take_comma_separated_names();
3641                self.consume_until_statement_boundary();
3642                Ok(Statement::ValidateOnly {
3643                    kind: crate::ast::ValidateOnlyKind::LockTable,
3644                    names,
3645                })
3646            }
3647            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3648            // durability marker + snapshot in PG. SPG has WAL
3649            // checkpointing on a byte / time schedule (v7.37.10
3650            // 60s / 4 MiB defaults). The bare statement parses to
3651            // `Statement::Empty` here (the no_std engine owns no
3652            // WAL / snapshot); v7.37 Epic Du wires the HOST
3653            // (embedded `Database::execute_buffered`, via
3654            // `sql_is_checkpoint`) to force an immediate synchronous
3655            // checkpoint through `Database::checkpoint` — a real
3656            // durability barrier, matching PG.
3657            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3658                self.advance();
3659                self.consume_until_statement_boundary();
3660                Ok(Statement::Empty)
3661            }
3662            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3663                self.advance();
3664                self.parse_delete_after_keyword()
3665            }
3666            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3667            // ALTER is not a reserved keyword in the lexer — handled
3668            // as a bare ident here.
3669            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3670                self.advance();
3671                self.parse_alter_after_keyword()
3672            }
3673            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3674            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3675            // additions needed.
3676            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3677                self.advance();
3678                self.parse_wait_after_keyword()
3679            }
3680            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3681            // Bare ANALYZE → analyse every user table; ANALYZE
3682            // <name> → re-stats one. The argument is an optional
3683            // ident (or quoted ident); anything else is a parse
3684            // error.
3685            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3686            // `WHERE` filter (carved out per V6_7_DESIGN.md
3687            // STABILITY). Lex order: identifier "compact" → "cold"
3688            // → "segments". Anything else after `COMPACT` is a
3689            // parse error.
3690            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3691                self.advance();
3692                let next = self.peek().clone();
3693                let cold = match next {
3694                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3695                    _ => {
3696                        return Err(
3697                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3698                        );
3699                    }
3700                };
3701                if !cold.eq_ignore_ascii_case("cold") {
3702                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3703                }
3704                self.advance();
3705                let next = self.peek().clone();
3706                let segments = match next {
3707                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3708                    _ => {
3709                        return Err(self.err(format!(
3710                            "expected SEGMENTS after COMPACT COLD, got {:?}",
3711                            self.peek()
3712                        )));
3713                    }
3714                };
3715                if !segments.eq_ignore_ascii_case("segments") {
3716                    return Err(self.err(format!(
3717                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3718                    )));
3719                }
3720                self.advance();
3721                Ok(Statement::CompactColdSegments)
3722            }
3723            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3724            // Parsed as a case-insensitive identifier since MERGE
3725            // isn't a reserved lexer keyword (collides with the
3726            // mysqldump `ALGORITHM = MERGE` view clause if it
3727            // were); the inner parser drives the rest of the
3728            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3729            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3730                self.advance();
3731                self.parse_merge_after_keyword()
3732            }
3733            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3734                self.advance();
3735                let target = match self.peek() {
3736                    Token::Eof | Token::Semicolon => None,
3737                    Token::Ident(_) | Token::QuotedIdent(_) => {
3738                        Some(self.expect_ident_like()?)
3739                    }
3740                    other => {
3741                        return Err(self.err(format!(
3742                            "expected table name or end of statement after ANALYZE, got {other:?}"
3743                        )));
3744                    }
3745                };
3746                // v7.39 (round 776, F31 J7) — the per-column form
3747                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3748                // here while the VACUUM arm already consumed it; SPG
3749                // analyzes whole tables, so the list parses and is
3750                // accepted like the VACUUM path's.
3751                if target.is_some() && matches!(self.peek(), Token::LParen) {
3752                    self.advance();
3753                    loop {
3754                        let _ = self.expect_ident_like()?;
3755                        match self.peek() {
3756                            Token::Comma => {
3757                                self.advance();
3758                            }
3759                            Token::RParen => {
3760                                self.advance();
3761                                break;
3762                            }
3763                            other => {
3764                                return Err(self.err(format!(
3765                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
3766                                )));
3767                            }
3768                        }
3769                    }
3770                }
3771                Ok(Statement::Analyze(target))
3772            }
3773            // v7.12.1 — `SET <name> [TO|=] <value>`. The
3774            // `default_text_search_config` parameter is consumed
3775            // by the FTS function dispatcher; other parameter
3776            // names are recorded but treated as a no-op so PG
3777            // dump output loads.
3778            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3779                self.advance();
3780                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3781                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3782                // …` which the SessionVar path handles). `LOCAL` is the only
3783                // one that changes semantics — it scopes the change to the
3784                // current transaction — so capture it; SESSION / GLOBAL are
3785                // accepted and treated as the default session scope.
3786                let mut set_local = false;
3787                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3788                    let q = s.to_ascii_lowercase();
3789                    if q == "local" || q == "session" || q == "global" {
3790                        set_local = q == "local";
3791                        self.advance();
3792                    }
3793                }
3794                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3795                // <collation>]` — change the connection client
3796                // charset. SPG stores UTF-8 always and orders
3797                // bytewise; accept as a no-op.
3798                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3799                {
3800                    self.advance();
3801                    // Charset ident-or-string.
3802                    if matches!(
3803                        self.peek(),
3804                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3805                    ) {
3806                        self.advance();
3807                    }
3808                    // Optional `COLLATE <name>`.
3809                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
3810                    {
3811                        self.advance();
3812                        if matches!(
3813                            self.peek(),
3814                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3815                        ) {
3816                            self.advance();
3817                        }
3818                    }
3819                    return Ok(Statement::Empty);
3820                }
3821                // v7.37.17 (17.6 sibling) — PG `SET ROLE
3822                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
3823                // uses this to switch to the object owner before
3824                // recreating tables. SPG has no role system so this
3825                // is a no-op.
3826                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
3827                {
3828                    self.advance(); // ROLE
3829                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
3830                    // reset to the login identity; a name / string sets the
3831                    // effective role that drives current_user + RLS.
3832                    let role = match self.peek().clone() {
3833                        Token::Default => {
3834                            self.advance();
3835                            None
3836                        }
3837                        Token::Ident(s) | Token::QuotedIdent(s)
3838                            if s.eq_ignore_ascii_case("none") =>
3839                        {
3840                            self.advance();
3841                            None
3842                        }
3843                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3844                            self.advance();
3845                            Some(s)
3846                        }
3847                        _ => None,
3848                    };
3849                    return Ok(Statement::SetRole(role));
3850                }
3851                // v7.37.17 (17.6 sibling) — PG `SET SESSION
3852                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
3853                // ISO SQL surface). pg_dump prepends this to fix
3854                // the isolation level for the restore session. SPG
3855                // defaults to READ COMMITTED and doesn't yet honor
3856                // session-set isolation across statements — accept
3857                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
3858                // per-tx form is handled elsewhere.
3859                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
3860                {
3861                    self.advance(); // CHARACTERISTICS
3862                    self.consume_until_statement_boundary();
3863                    return Ok(Statement::Empty);
3864                }
3865                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
3866                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
3867                // pg_dump emits this to control the deferrability of
3868                // FK / UNIQUE constraints across a bulk restore. SPG
3869                // has no deferrable-constraint machinery today; the
3870                // FK checker is strict-immediate. Accept-and-no-op
3871                // for pg_dump round-trip compatibility.
3872                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
3873                {
3874                    self.advance(); // CONSTRAINTS
3875                    // v7.39 (round 288) — no longer a no-op: the trailing
3876                    // DEFERRED / IMMEDIATE sets the transaction's timing.
3877                    // v7.39 (round 308, V29) — and the names are kept.
3878                    // They used to be skipped over on the way to the
3879                    // DEFERRED keyword, so a named form silently behaved
3880                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
3881                    // every deferrable constraint in the transaction.
3882                    let mut names: alloc::vec::Vec<alloc::string::String> =
3883                        alloc::vec::Vec::new();
3884                    if matches!(self.peek(), Token::All) {
3885                        self.advance();
3886                    } else {
3887                        loop {
3888                            let mut n = self.expect_ident_like()?;
3889                            // A schema-qualified name (`public.fk_a`)
3890                            // identifies the same constraint; PG resolves
3891                            // it by the trailing segment.
3892                            while matches!(self.peek(), Token::Dot) {
3893                                self.advance();
3894                                n = self.expect_ident_like()?;
3895                            }
3896                            names.push(n);
3897                            if matches!(self.peek(), Token::Comma) {
3898                                self.advance();
3899                            } else {
3900                                break;
3901                            }
3902                        }
3903                    }
3904                    let deferred = match self.peek() {
3905                        Token::Ident(s) | Token::QuotedIdent(s)
3906                            if s.eq_ignore_ascii_case("deferred") =>
3907                        {
3908                            true
3909                        }
3910                        Token::Ident(s) | Token::QuotedIdent(s)
3911                            if s.eq_ignore_ascii_case("immediate") =>
3912                        {
3913                            false
3914                        }
3915                        other => {
3916                            return Err(self.err(alloc::format!(
3917                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
3918                            )));
3919                        }
3920                    };
3921                    self.advance();
3922                    return Ok(Statement::SetConstraints { names, deferred });
3923                }
3924                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
3925                // { DEFAULT | '<role>' | <ident> }` (mailrs
3926                // round-10 A.1). pg_dump preamble emits the
3927                // `DEFAULT` form to reset session authorization.
3928                //
3929                // v7.39 (round 697) — this said "SPG has no role system so
3930                // this is a strict no-op". SPG has had one since round 58;
3931                // the comment outlived it, and with it the reason a name
3932                // that is not a role was accepted here. It still switches
3933                // no authorization — what it does now is refuse a role
3934                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
3935                // AUTHORIZATION` (handled by the RESET parser
3936                // elsewhere). Reference:
3937                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
3938                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
3939                {
3940                    self.advance(); // AUTHORIZATION
3941                    match self.peek().clone() {
3942                        Token::Default => {
3943                            self.advance();
3944                        }
3945                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
3946                            self.advance();
3947                            return Ok(Statement::ValidateOnly {
3948                                kind: crate::ast::ValidateOnlyKind::RoleName,
3949                                names: alloc::vec![r],
3950                            });
3951                        }
3952                        other => {
3953                            return Err(self.err(alloc::format!(
3954                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
3955                            )));
3956                        }
3957                    }
3958                    return Ok(Statement::Empty);
3959                }
3960                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
3961                // ISOLATION LEVEL { READ COMMITTED | READ
3962                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
3963                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
3964                // PG-standard surface. v7.37.8 accepts the syntax
3965                // and tracks the selected level on
3966                // `Engine::current_isolation_level()`; the actual
3967                // MVCC / SSI semantics implementation lands in
3968                // the 轴 4 isolation framework (separate train).
3969                // PG itself maps READ UNCOMMITTED to READ COMMITTED
3970                // internally; SPG behaves the same (effectively
3971                // READ COMMITTED at every level today).
3972                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
3973                {
3974                    self.advance(); // TRANSACTION
3975                    let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
3976                    return Ok(Statement::SetTransaction { isolation: level });
3977                }
3978                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
3979                // alias — same accept-as-no-op as SET NAMES.
3980                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
3981                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
3982                {
3983                    self.advance(); // CHARACTER
3984                    self.advance(); // SET
3985                    if matches!(
3986                        self.peek(),
3987                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3988                    ) {
3989                        self.advance();
3990                    }
3991                    return Ok(Statement::Empty);
3992                }
3993                // v7.39 (GUC) — PG spells the timezone GUC as two
3994                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
3995                // where <value> is a string/ident or the LOCAL /
3996                // DEFAULT keyword (both mean "back to the default").
3997                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
3998                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
3999                {
4000                    self.advance(); // TIME
4001                    self.advance(); // ZONE
4002                    let value = match self.peek().clone() {
4003                        Token::Ident(s)
4004                            if s.eq_ignore_ascii_case("local")
4005                                || s.eq_ignore_ascii_case("default") =>
4006                        {
4007                            self.advance();
4008                            crate::ast::SetValue::Default
4009                        }
4010                        Token::Default => {
4011                            self.advance();
4012                            crate::ast::SetValue::Default
4013                        }
4014                        _ => self.parse_set_value()?,
4015                    };
4016                    return Ok(Statement::SetParameter {
4017                        name: "timezone".into(),
4018                        value,
4019                        local: set_local,
4020                    });
4021                }
4022                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4023                // MySQL USER-variable assignment: its own per-session
4024                // namespace, an arbitrary expression on the right, and `:=`
4025                // as a second spelling of `=`. It used to fall into the
4026                // session-PARAMETER list below, whose values are literals and
4027                // whose store nothing reads back under a `@` name — so the
4028                // assignment reported success and vanished.
4029                //
4030                // A `@@`-prefixed LHS is a real engine setting and keeps the
4031                // old path.
4032                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4033                    return self.parse_set_user_vars();
4034                }
4035                // v7.14.0 — multi-assignment form
4036                // `SET a = 1, b = 2, …`. Single-assignment is the
4037                // 1-element case. Each LHS may be a regular ident
4038                // or a SessionVar (`@VAR` / `@@VAR`).
4039                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4040                loop {
4041                    let lhs = match self.peek().clone() {
4042                        Token::SessionVar(s) => {
4043                            self.advance();
4044                            s
4045                        }
4046                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4047                        other => {
4048                            return Err(self.err(format!(
4049                                "expected parameter name after SET, got {other:?}"
4050                            )));
4051                        }
4052                    };
4053                    // Accept either `=` or the bare `TO` keyword.
4054                    match self.peek() {
4055                        Token::Eq => {
4056                            self.advance();
4057                        }
4058                        Token::To => {
4059                            self.advance();
4060                        }
4061                        other => {
4062                            return Err(self.err(format!(
4063                                "expected `=` or TO after SET {lhs}, got {other:?}"
4064                            )));
4065                        }
4066                    }
4067                    let mut value = self.parse_set_value()?;
4068                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4069                    // `, name TO` continues a MySQL-style multi-assign,
4070                    // anything else is a PG list VALUE
4071                    // (`SET search_path = myschema, public`) folded into
4072                    // one comma-joined string.
4073                    while matches!(self.peek(), Token::Comma) {
4074                        let is_assign = matches!(
4075                            self.tokens.get(self.pos + 1),
4076                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4077                        ) && matches!(
4078                            self.tokens.get(self.pos + 2),
4079                            Some(Token::Eq | Token::To)
4080                        );
4081                        if is_assign {
4082                            break;
4083                        }
4084                        self.advance(); // comma
4085                        let next = self.parse_set_value()?;
4086                        let joined = alloc::format!(
4087                            "{}, {}",
4088                            set_value_text(&value),
4089                            set_value_text(&next)
4090                        );
4091                        value = crate::ast::SetValue::String(joined);
4092                    }
4093                    pairs.push((lhs, value));
4094                    if matches!(self.peek(), Token::Comma) {
4095                        self.advance();
4096                        continue;
4097                    }
4098                    break;
4099                }
4100                if pairs.len() == 1 {
4101                    let (name, value) = pairs.into_iter().next().unwrap();
4102                    Ok(Statement::SetParameter {
4103                        name,
4104                        value,
4105                        local: set_local,
4106                    })
4107                } else {
4108                    Ok(Statement::SetParameterList(pairs))
4109                }
4110            }
4111            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4112            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4113                self.advance();
4114                match self.peek().clone() {
4115                    Token::All => {
4116                        self.advance();
4117                        Ok(Statement::ResetParameter(None))
4118                    }
4119                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4120                        self.advance();
4121                        Ok(Statement::ResetParameter(None))
4122                    }
4123                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4124                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4125                        self.advance();
4126                        Ok(Statement::SetRole(None))
4127                    }
4128                    _ => {
4129                        let name = self.parse_set_param_name()?;
4130                        Ok(Statement::ResetParameter(Some(name)))
4131                    }
4132                }
4133            }
4134            // v7.39 (round 218) — server-side cursors.
4135            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4136            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4137            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4138            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4139                self.advance();
4140                match self.peek().clone() {
4141                    Token::All => {
4142                        self.advance();
4143                        Ok(Statement::CloseCursor { name: None })
4144                    }
4145                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4146                        self.advance();
4147                        Ok(Statement::CloseCursor { name: None })
4148                    }
4149                    Token::Ident(n) | Token::QuotedIdent(n) => {
4150                        self.advance();
4151                        Ok(Statement::CloseCursor { name: Some(n) })
4152                    }
4153                    other => Err(self.err(format!(
4154                        "expected cursor name or ALL after CLOSE, got {other:?}"
4155                    ))),
4156                }
4157            }
4158            other => Err(self.err(format!(
4159                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4160                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4161            ))),
4162        }
4163    }
4164
4165    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4166    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4167    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4168    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4169    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4170        self.advance(); // DECLARE
4171        let name = match self.advance() {
4172            Token::Ident(n) | Token::QuotedIdent(n) => n,
4173            other => {
4174                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4175            }
4176        };
4177        let mut scroll: Option<bool> = None;
4178        loop {
4179            match self.peek() {
4180                Token::Ident(s)
4181                    if s.eq_ignore_ascii_case("binary")
4182                        || s.eq_ignore_ascii_case("insensitive")
4183                        || s.eq_ignore_ascii_case("asensitive") =>
4184                {
4185                    self.advance();
4186                }
4187                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4188                    self.advance();
4189                    scroll = Some(true);
4190                }
4191                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4192                {
4193                    self.advance(); // NO
4194                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4195                        return Err(self.err(format!(
4196                            "expected SCROLL after NO in DECLARE, got {:?}",
4197                            self.peek()
4198                        )));
4199                    }
4200                    self.advance();
4201                    scroll = Some(false);
4202                }
4203                _ => break,
4204            }
4205        }
4206        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4207            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4208        }
4209        self.advance();
4210        let mut hold = false;
4211        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4212            self.advance();
4213            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4214                return Err(self.err(format!(
4215                    "expected HOLD after WITH in DECLARE, got {:?}",
4216                    self.peek()
4217                )));
4218            }
4219            self.advance();
4220            hold = true;
4221        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4222            self.advance();
4223            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4224                return Err(self.err(format!(
4225                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4226                    self.peek()
4227                )));
4228            }
4229            self.advance();
4230        }
4231        if !matches!(self.peek(), Token::For) {
4232            return Err(self.err(format!(
4233                "expected FOR before the cursor query, got {:?}",
4234                self.peek()
4235            )));
4236        }
4237        self.advance();
4238        let query = self.parse_one_statement()?;
4239        Ok(Statement::DeclareCursor {
4240            name,
4241            scroll,
4242            hold,
4243            query: alloc::boxed::Box::new(query),
4244        })
4245    }
4246
4247    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4248    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4249    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4250    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4251        use crate::ast::CursorDirection as D;
4252        self.advance(); // FETCH / MOVE
4253        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4254            let neg = if matches!(this.peek(), Token::Minus) {
4255                this.advance();
4256                true
4257            } else {
4258                false
4259            };
4260            match this.advance() {
4261                Token::Integer(v) => Ok(if neg { -v } else { v }),
4262                other => Err(this.err(format!("expected count, got {other:?}"))),
4263            }
4264        };
4265        let direction = match self.peek().clone() {
4266            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4267                self.advance();
4268                D::Next
4269            }
4270            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4271                self.advance();
4272                D::Prior
4273            }
4274            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4275                self.advance();
4276                D::First
4277            }
4278            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4279                self.advance();
4280                D::Last
4281            }
4282            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4283                self.advance();
4284                D::Absolute(signed_count(self)?)
4285            }
4286            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4287                self.advance();
4288                D::Relative(signed_count(self)?)
4289            }
4290            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4291                self.advance();
4292                match self.peek().clone() {
4293                    Token::All => {
4294                        self.advance();
4295                        D::All
4296                    }
4297                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4298                        self.advance();
4299                        D::All
4300                    }
4301                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4302                    _ => D::Next, // bare FORWARD = FORWARD 1
4303                }
4304            }
4305            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4306                self.advance();
4307                match self.peek().clone() {
4308                    Token::All => {
4309                        self.advance();
4310                        D::BackwardAll
4311                    }
4312                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4313                        self.advance();
4314                        D::BackwardAll
4315                    }
4316                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4317                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4318                }
4319            }
4320            Token::All => {
4321                self.advance();
4322                D::All
4323            }
4324            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4325                self.advance();
4326                D::All
4327            }
4328            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4329            // Bare `FETCH <name>` — direction defaults to NEXT.
4330            _ => D::Next,
4331        };
4332        // Optional FROM / IN.
4333        if matches!(self.peek(), Token::From)
4334            || matches!(self.peek(), Token::In)
4335            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4336        {
4337            self.advance();
4338        }
4339        let name = match self.advance() {
4340            Token::Ident(n) | Token::QuotedIdent(n) => n,
4341            other => {
4342                return Err(self.err(format!("expected cursor name, got {other:?}")));
4343            }
4344        };
4345        Ok(if is_move {
4346            Statement::MoveCursor { name, direction }
4347        } else {
4348            Statement::FetchCursor { name, direction }
4349        })
4350    }
4351
4352    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4353    /// [(kind, …)] ON <col>, … FROM <table>`.
4354    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4355        self.advance(); // STATISTICS
4356        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4357        let mut if_not_exists = false;
4358        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4359            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4360        {
4361            self.advance();
4362            self.advance();
4363            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4364                self.advance();
4365                if_not_exists = true;
4366            }
4367        }
4368        let name = self.expect_ident_like()?;
4369        let mut kinds = Vec::new();
4370        if matches!(self.peek(), Token::LParen) {
4371            self.advance();
4372            loop {
4373                let k = self.expect_ident_like()?;
4374                // PG stores the single letters; accept the spelled-out
4375                // names the SQL uses and record what PG records.
4376                kinds.push(match k.to_ascii_lowercase().as_str() {
4377                    "ndistinct" => String::from("d"),
4378                    "dependencies" => String::from("f"),
4379                    "mcv" => String::from("m"),
4380                    other => {
4381                        return Err(
4382                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4383                        );
4384                    }
4385                });
4386                match self.advance() {
4387                    Token::Comma => {}
4388                    Token::RParen => break,
4389                    other => {
4390                        return Err(self.err(alloc::format!(
4391                            "expected ',' or ')' in statistics kind list, got {other:?}"
4392                        )));
4393                    }
4394                }
4395            }
4396        }
4397        if !matches!(self.peek(), Token::On) {
4398            return Err(self.err(alloc::format!(
4399                "expected ON in CREATE STATISTICS, got {:?}",
4400                self.peek()
4401            )));
4402        }
4403        self.advance();
4404        let mut columns = Vec::new();
4405        loop {
4406            columns.push(self.expect_ident_like()?);
4407            if matches!(self.peek(), Token::Comma) {
4408                self.advance();
4409            } else {
4410                break;
4411            }
4412        }
4413        if !matches!(self.peek(), Token::From) {
4414            return Err(self.err(alloc::format!(
4415                "expected FROM in CREATE STATISTICS, got {:?}",
4416                self.peek()
4417            )));
4418        }
4419        self.advance();
4420        let table = self.expect_ident_like()?;
4421        Ok(Statement::CreateStatistics {
4422            name,
4423            if_not_exists,
4424            kinds,
4425            columns,
4426            table,
4427        })
4428    }
4429
4430    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4431    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4432    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4433    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4434    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4435    /// forward call.
4436    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4437        self.advance(); // TABLE
4438        let if_exists = self.consume_if_exists();
4439        let mut names: Vec<String> = Vec::new();
4440        loop {
4441            names.push(self.expect_ident_like()?);
4442            if matches!(self.peek(), Token::Comma) {
4443                self.advance();
4444                continue;
4445            }
4446            break;
4447        }
4448        if matches!(
4449            self.peek(),
4450            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4451                || s.eq_ignore_ascii_case("restrict")
4452        ) {
4453            self.advance();
4454        }
4455        Ok(Statement::DropTable { names, if_exists })
4456    }
4457
4458    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4459        self.advance(); // STATISTICS
4460        let mut if_exists = false;
4461        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4462            && matches!(self.tokens.get(self.pos + 1),
4463                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4464        {
4465            self.advance();
4466            self.advance();
4467            if_exists = true;
4468        }
4469        let name = self.expect_ident_like()?;
4470        Ok(Statement::DropStatistics { name, if_exists })
4471    }
4472
4473    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4474        debug_assert!(matches!(self.peek(), Token::Create));
4475        self.advance();
4476        match self.peek() {
4477            Token::Table => self.parse_create_table_stmt_after_create(),
4478            Token::Index => self.parse_create_index_stmt_after_create(false),
4479            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4480            // object now. It used to be consumed by the CREATE-noise
4481            // arm, so a pg_dump that declares extended statistics
4482            // restored silently without them and reflection showed
4483            // nothing.
4484            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4485                self.parse_create_statistics_after_create()
4486            }
4487            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4488            // The `UNIQUE` modifier turns a partial index into a
4489            // partial-uniqueness invariant (only rows matching the
4490            // WHERE predicate are checked for duplicates). mailrs
4491            // K1 (3 hits: email_templates default, calendar_events
4492            // master, calendar_events instance).
4493            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4494                self.advance();
4495                if !matches!(self.peek(), Token::Index) {
4496                    return Err(self.err(alloc::format!(
4497                        "expected INDEX after CREATE UNIQUE, got {:?}",
4498                        self.peek()
4499                    )));
4500                }
4501                self.parse_create_index_stmt_after_create(true)
4502            }
4503            Token::Publication => {
4504                self.advance();
4505                self.parse_create_publication_after_keyword()
4506            }
4507            Token::Subscription => {
4508                self.advance();
4509                self.parse_create_subscription_after_keyword()
4510            }
4511            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4512            // USER isn't a reserved keyword — we look for the bare
4513            // identifier so the lexer doesn't have to grow a token.
4514            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4515                self.advance();
4516                self.parse_create_user_after_keyword(true)
4517            }
4518            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4519            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4520            // the default of the LOGIN attribute.
4521            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4522                self.advance();
4523                self.parse_create_user_after_keyword(false)
4524            }
4525            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4526            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4527                self.advance();
4528                self.parse_create_policy_after_keyword()
4529            }
4530            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4531            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4532            // no-op. mailrs follow-up F3.
4533            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4534                self.advance();
4535                self.parse_create_extension_after_keyword()
4536            }
4537            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4538            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4539            // optional; absorb it here and forward to the
4540            // per-kind parsers with the flag. OR is a reserved
4541            // keyword token.
4542            Token::Or => {
4543                self.advance();
4544                let next = self.peek();
4545                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4546                    return Err(self.err(alloc::format!(
4547                        "expected REPLACE after CREATE OR, got {next:?}"
4548                    )));
4549                };
4550                if !s2.eq_ignore_ascii_case("replace") {
4551                    return Err(self.err(alloc::format!(
4552                        "expected REPLACE after CREATE OR, got {s2:?}"
4553                    )));
4554                }
4555                self.advance();
4556                self.parse_create_function_or_trigger_after_or_replace(true)
4557            }
4558            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4559                self.advance();
4560                self.parse_create_function_after_keyword(false)
4561            }
4562            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4563                self.advance();
4564                self.parse_create_trigger_after_keyword(false)
4565            }
4566            // v7.39 (round 139) — CREATE RULE …
4567            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4568                self.advance();
4569                self.parse_create_rule_after_keyword(false)
4570            }
4571            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4572            // trigger is a row-level AFTER trigger that additionally carries
4573            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4574            // path already tolerates and skips those clauses, so consuming the
4575            // CONSTRAINT keyword and reusing it makes the statement parse and the
4576            // trigger fire. (The deferral timing itself is not yet honoured —
4577            // SPG fires it as a plain AFTER trigger, which is correct behaviour
4578            // for every non-deferred use.)
4579            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4580                self.advance();
4581                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4582                    if t.eq_ignore_ascii_case("trigger"))
4583                {
4584                    return Err(self.err(alloc::format!(
4585                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4586                        self.peek()
4587                    )));
4588                }
4589                self.advance();
4590                self.parse_create_trigger_after_keyword(false)
4591            }
4592            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4593            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4594                self.advance();
4595                self.parse_create_sequence_after_keyword(false)
4596            }
4597            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4598            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4599                self.advance();
4600                self.parse_create_view_after_keyword(false, false, false)
4601            }
4602            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4603            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4604            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4605            // appear (in any order) between `CREATE` and `VIEW` in
4606            // every mysqldump-emitted view. Pre-2.6 the parser
4607            // rejected the prefix and the customer's whole view
4608            // backup failed on the first view. The hints are pure
4609            // planner / permission metadata; SPG's view-rewrite
4610            // path is semantically equivalent for all three
4611            // algorithms in v7.17 (TEMPTABLE differs only in
4612            // perf for huge views — out of v7.17 scope), and
4613            // DEFINER / SQL SECURITY are pure single-user
4614            // permissioning that SPG ignores by design.
4615            Token::Ident(s) | Token::QuotedIdent(s)
4616                if s.eq_ignore_ascii_case("algorithm")
4617                    || s.eq_ignore_ascii_case("definer")
4618                    || s.eq_ignore_ascii_case("sql") =>
4619            {
4620                self.consume_mysql_view_prefix()?;
4621                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4622                // (in any order, in any combination), the next
4623                // keyword must be VIEW. mysqldump never emits these
4624                // prefixes on non-view statements.
4625                let next = self.peek().clone();
4626                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4627                    if s2.eq_ignore_ascii_case("view"))
4628                {
4629                    self.advance();
4630                    self.parse_create_view_after_keyword(false, false, false)
4631                } else {
4632                    Err(self.err(alloc::format!(
4633                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4634                    )))
4635                }
4636            }
4637            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4638            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4639                self.advance();
4640                self.parse_create_type_after_keyword()
4641            }
4642            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4643            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4644            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4645                self.advance();
4646                self.parse_create_domain_after_keyword()
4647            }
4648            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4649            // name [AUTHORIZATION user]. Real catalog registry
4650            // (was silent-no-op'd pre-v7.17).
4651            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4652                self.advance();
4653                let if_not_exists = self.parse_if_not_exists();
4654                let name = self.expect_ident_like()?;
4655                // Optional `AUTHORIZATION <user>` trailer — accepted,
4656                // ignored (single-user catalog).
4657                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4658                    if s.eq_ignore_ascii_case("authorization"))
4659                {
4660                    self.advance();
4661                    let _ = self.expect_ident_like()?;
4662                }
4663                Ok(Statement::CreateSchema { name, if_not_exists })
4664            }
4665            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4666            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4667                self.advance();
4668                let next = self.peek().clone();
4669                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4670                {
4671                    self.advance();
4672                    self.parse_create_materialized_view_after_keyword()
4673                } else {
4674                    Err(self.err(alloc::format!(
4675                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4676                    )))
4677                }
4678            }
4679            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4680            // no-op below), an UNLOGGED table is a real, fully-usable table in
4681            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4682            // durability optimisation is a follow-up), so a dump / app that
4683            // declares UNLOGGED tables works instead of failing to parse.
4684            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4685                self.advance(); // UNLOGGED
4686                if matches!(self.peek(), Token::Table) {
4687                    self.parse_create_table_stmt_after_create()
4688                } else {
4689                    Err(self.err(format!(
4690                        "expected TABLE after CREATE UNLOGGED, got {:?}",
4691                        self.peek()
4692                    )))
4693                }
4694            }
4695            Token::Ident(s) | Token::QuotedIdent(s)
4696                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4697            {
4698                self.advance();
4699                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4700                let next = self.peek().clone();
4701                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4702                {
4703                    self.advance();
4704                    self.parse_create_sequence_after_keyword(true)
4705                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4706                {
4707                    self.advance();
4708                    self.parse_create_view_after_keyword(false, false, true)
4709                } else {
4710                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4711                    // consumed and answered OK while creating nothing, so
4712                    // every statement that touched the table afterwards failed
4713                    // with "table not found" — the DDL itself lied. It is a
4714                    // real CREATE TABLE now, marked temporary so the executor
4715                    // puts it in the session's own namespace. An optional
4716                    // TABLE keyword may or may not be present (`CREATE TEMP t`
4717                    // is not legal, but the keyword is consumed by the
4718                    // CREATE TABLE parser itself).
4719                    let stmt = self.parse_create_table_stmt_after_create()?;
4720                    match stmt {
4721                        Statement::CreateTable(mut c) => {
4722                            c.temporary = true;
4723                            Ok(Statement::CreateTable(c))
4724                        }
4725                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4726                        // CTAS node, which needs the same session namespace.
4727                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4728                            m.temporary = true;
4729                            Ok(Statement::CreateMaterializedView(m))
4730                        }
4731                        other => Ok(other),
4732                    }
4733                }
4734            }
4735            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4736            // BEGIN <body> END`. The body may reference `@var`
4737            // session variables, SET statements, internal `;`
4738            // terminators, etc. SPG has no procedure runtime, so
4739            // consume the whole `CREATE PROCEDURE … END` block as
4740            // a no-op so mysqldump scripts that include stored
4741            // routines load through. The matching-END consumer
4742            // tracks BEGIN/END nesting depth to handle nested
4743            // BEGIN blocks correctly.
4744            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4745                self.consume_mysql_routine_body();
4746                Ok(Statement::Empty)
4747            }
4748            // v7.14.0 — pg_dump / mysqldump emit
4749            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4750            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4751            // SPG is single-schema / single-database; these have
4752            // no behavioural effect, so consume + return Empty.
4753            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4754            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4755            // moved up to real parser branches. DATABASE / ROLE /
4756            // POLICY / OPERATOR stay no-op forever
4757            // (single-database, hardcoded roles).
4758            Token::Ident(s) | Token::QuotedIdent(s)
4759                if matches!(
4760                    s.to_ascii_lowercase().as_str(),
4761                    "database"
4762                        | "role"
4763                        | "operator"
4764                        | "cast"
4765                        | "aggregate"
4766                        | "language"
4767                        | "collation"
4768                        | "conversion"
4769                        // v7.17.0 Phase 8 (audit N6) — rarely-
4770                        // emitted pg_dump shapes that should
4771                        // load through without a parser error.
4772                        // SPG has no planner statistics catalog,
4773                        // no event-trigger hooks, no foreign-
4774                        // data-wrapper infrastructure; consume
4775                        // + return Empty.
4776                        | "statistics"
4777                        | "event"
4778                        // v7.37.17 (17.6 siblings) — additional CREATE
4779                        // targets pg_dump / operator install scripts
4780                        // may emit that SPG has no matching machinery
4781                        // for. Consume + Empty-return.
4782                        | "text"
4783                        | "tablespace"
4784                        | "access"
4785                        | "large"
4786                ) =>
4787            {
4788                // DATABASE is the one member of this list PG refuses
4789                // inside a transaction block; the rest (ROLE, CAST,
4790                // TABLESPACE, …) it runs there quite happily, so only
4791                // this one is named. Still a no-op otherwise — SPG is
4792                // single-database.
4793                let is_database = s.eq_ignore_ascii_case("database");
4794                self.consume_until_statement_boundary();
4795                if is_database {
4796                    return Ok(Statement::NoOpPreventedInTransaction {
4797                        what: String::from("CREATE DATABASE"),
4798                    });
4799                }
4800                Ok(Statement::Empty)
4801            }
4802            // v7.39 (round 706) — the foreign-data family leaves the silent
4803            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
4804            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
4805            // FDW machinery), but the ENGINE now warns, so a restore log
4806            // says what will not function instead of reporting success.
4807            Token::Ident(s) | Token::QuotedIdent(s)
4808                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
4809            {
4810                self.consume_until_statement_boundary();
4811                Ok(Statement::ValidateOnly {
4812                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
4813                    names: Vec::new(),
4814                })
4815            }
4816            other => Err(self.err(format!(
4817                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
4818            ))),
4819        }
4820    }
4821
4822    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
4823    /// keyword decides whether we parse a function or trigger
4824    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
4825    /// PROCEDURE) — those land in later releases.
4826    fn parse_create_function_or_trigger_after_or_replace(
4827        &mut self,
4828        or_replace: bool,
4829    ) -> Result<Statement, ParseError> {
4830        let tok = self.peek();
4831        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4832            return Err(self.err(alloc::format!(
4833                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
4834            )));
4835        };
4836        if s.eq_ignore_ascii_case("function") {
4837            self.advance();
4838            self.parse_create_function_after_keyword(or_replace)
4839        } else if s.eq_ignore_ascii_case("trigger") {
4840            self.advance();
4841            self.parse_create_trigger_after_keyword(or_replace)
4842        } else if s.eq_ignore_ascii_case("rule") {
4843            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
4844            self.advance();
4845            self.parse_create_rule_after_keyword(or_replace)
4846        } else if s.eq_ignore_ascii_case("view") {
4847            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
4848            self.advance();
4849            self.parse_create_view_after_keyword(or_replace, false, false)
4850        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
4851            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
4852            self.advance();
4853            let nxt = self.peek().clone();
4854            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
4855            {
4856                self.advance();
4857                self.parse_create_view_after_keyword(or_replace, false, true)
4858            } else {
4859                Err(self.err(alloc::format!(
4860                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
4861                )))
4862            }
4863        } else {
4864            Err(self.err(alloc::format!(
4865                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
4866            )))
4867        }
4868    }
4869
4870    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
4871    /// SPG doesn't have a registry; pgvector / similar are
4872    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
4873    /// the syntax lets dual-target schemas keep the line.
4874    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
4875        // Optional `IF NOT EXISTS`.
4876        self.consume_if_not_exists();
4877        let name = self.expect_ident_like()?;
4878        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
4879        // CASCADE / FROM '<v>' clauses; we don't model them.
4880        loop {
4881            match self.peek() {
4882                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
4883                    self.advance();
4884                    continue;
4885                }
4886                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
4887                    self.advance();
4888                    let _ = self.expect_ident_like()?;
4889                    continue;
4890                }
4891                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
4892                    self.advance();
4893                    // String or ident literal.
4894                    let _ = self.advance();
4895                    continue;
4896                }
4897                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
4898                    self.advance();
4899                    let _ = self.advance();
4900                    continue;
4901                }
4902                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
4903                    self.advance();
4904                    continue;
4905                }
4906                _ => break,
4907            }
4908        }
4909        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
4910        // nosuch` reported success and `pg_extension` then did not list it,
4911        // which is the accept-and-do-nothing shape F31 exists to find.
4912        Ok(Statement::ValidateOnly {
4913            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
4914            names: alloc::vec![name],
4915        })
4916    }
4917
4918    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
4919    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
4920    /// already been consumed by the caller. Grammar accepted:
4921    ///
4922    ///   name `(` arg-list `)`
4923    ///   `RETURNS` return-type
4924    ///   [ `LANGUAGE` ident ]
4925    ///   `AS` $$ body $$
4926    ///   [ `LANGUAGE` ident ]
4927    ///
4928    /// Either `LANGUAGE` position is allowed; PG accepts both.
4929    fn parse_create_function_after_keyword(
4930        &mut self,
4931        or_replace: bool,
4932    ) -> Result<Statement, ParseError> {
4933        let name = self.expect_ident_like()?;
4934        // Argument list. v7.12.4 commonly sees the empty `()`
4935        // (trigger functions); typed args parse and round-trip
4936        // but the executor only invokes nullary functions.
4937        if !matches!(self.peek(), Token::LParen) {
4938            return Err(self.err(alloc::format!(
4939                "expected '(' after function name {name:?}, got {:?}",
4940                self.peek()
4941            )));
4942        }
4943        self.advance();
4944        let args = self.parse_function_arg_list()?;
4945        // RETURNS clause.
4946        let tok = self.peek();
4947        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4948            return Err(self.err(alloc::format!(
4949                "expected RETURNS after function arg list, got {tok:?}"
4950            )));
4951        };
4952        if !s.eq_ignore_ascii_case("returns") {
4953            return Err(self.err(alloc::format!(
4954                "expected RETURNS after function arg list, got {s:?}"
4955            )));
4956        }
4957        self.advance();
4958        let returns = self.parse_function_return()?;
4959        // Optional LANGUAGE clause (PG also accepts after AS — we'll
4960        // re-check after the body too).
4961        let mut language: Option<String> = self.parse_optional_language()?;
4962        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
4963        // either side of the body and in any order, interleaved with
4964        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
4965        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
4966        // PG's own pg_dump output did not restore.
4967        let mut attrs = FunctionAttrs::default();
4968        loop {
4969            let before = self.pos;
4970            self.parse_function_attrs_into(&mut attrs)?;
4971            if language.is_none() {
4972                language = self.parse_optional_language()?;
4973            }
4974            if self.pos == before {
4975                break;
4976            }
4977        }
4978        // `AS` followed by a $$-quoted body (lexer already
4979        // collapses both `$$…$$` and `$tag$…$tag$` to a single
4980        // Token::String). AS is a reserved keyword (Token::As).
4981        if !matches!(self.peek(), Token::As) {
4982            return Err(self.err(alloc::format!(
4983                "expected AS before function body, got {:?}",
4984                self.peek()
4985            )));
4986        }
4987        self.advance();
4988        let body_text = match self.peek() {
4989            Token::String(s) => {
4990                let body = s.clone();
4991                self.advance();
4992                body
4993            }
4994            other => {
4995                return Err(self.err(alloc::format!(
4996                    "expected $$-quoted function body after AS, got {other:?}"
4997                )));
4998            }
4999        };
5000        // Trailing clauses — PG's other accepted position for both the
5001        // LANGUAGE and the attributes.
5002        loop {
5003            let before = self.pos;
5004            self.parse_function_attrs_into(&mut attrs)?;
5005            if language.is_none() {
5006                language = self.parse_optional_language()?;
5007            }
5008            if self.pos == before {
5009                break;
5010            }
5011        }
5012        let language = language.unwrap_or_else(|| String::from("sql"));
5013        // PL/pgSQL bodies get structure-parsed. Other languages
5014        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5015        // recognise) round-trip as Raw text — the executor errors
5016        // when invoked with a clear unsupported message.
5017        let body = if language.eq_ignore_ascii_case("plpgsql") {
5018            match parse_plpgsql_body(&body_text) {
5019                Ok(block) => FunctionBody::PlPgSql(block),
5020                // Best-effort: if the body parser doesn't yet
5021                // support a construct used inside, fall back to
5022                // raw — keeps `CREATE FUNCTION` itself working
5023                // (catalogue accepts), executor errors on
5024                // invocation only.
5025                Err(_) => FunctionBody::Raw(body_text),
5026            }
5027        } else {
5028            FunctionBody::Raw(body_text)
5029        };
5030        Ok(Statement::CreateFunction(CreateFunctionStatement {
5031            name,
5032            or_replace,
5033            args,
5034            returns,
5035            language,
5036            body,
5037            attrs,
5038        }))
5039    }
5040
5041    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5042    /// attribute clauses into `attrs`, stopping at the first token that
5043    /// is not one. Measured against PG 18.4, which accepts them in any
5044    /// order and on either side of the body.
5045    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5046        loop {
5047            let word = match self.peek() {
5048                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5049                // NOT LEAKPROOF — NOT is a reserved keyword token.
5050                Token::Not
5051                    if matches!(
5052                        self.tokens.get(self.pos + 1),
5053                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5054                    ) =>
5055                {
5056                    self.advance();
5057                    self.advance();
5058                    attrs.leakproof = false;
5059                    continue;
5060                }
5061                _ => return Ok(()),
5062            };
5063            match word.as_str() {
5064                "immutable" => {
5065                    self.advance();
5066                    attrs.volatility = FunctionVolatility::Immutable;
5067                }
5068                "stable" => {
5069                    self.advance();
5070                    attrs.volatility = FunctionVolatility::Stable;
5071                }
5072                "volatile" => {
5073                    self.advance();
5074                    attrs.volatility = FunctionVolatility::Volatile;
5075                }
5076                "strict" => {
5077                    self.advance();
5078                    attrs.strict = true;
5079                }
5080                "leakproof" => {
5081                    self.advance();
5082                    attrs.leakproof = true;
5083                }
5084                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5085                // spelled-out forms of STRICT and its opposite.
5086                "returns" | "called" => {
5087                    let strict = word == "returns";
5088                    let mut probe = self.pos + 1;
5089                    if strict {
5090                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5091                        // is not ours.
5092                        match self.tokens.get(probe) {
5093                            Some(Token::Null) => probe += 1,
5094                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5095                            _ => return Ok(()),
5096                        }
5097                    }
5098                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5099                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5100                    if !ok {
5101                        return Ok(());
5102                    }
5103                    probe += 1;
5104                    match self.tokens.get(probe) {
5105                        Some(Token::Null) => probe += 1,
5106                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5107                        _ => return Ok(()),
5108                    }
5109                    match self.tokens.get(probe) {
5110                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5111                        _ => return Ok(()),
5112                    }
5113                    self.pos = probe;
5114                    attrs.strict = strict;
5115                }
5116                "security" | "external" => {
5117                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5118                    let mut probe = self.pos + 1;
5119                    if word == "external" {
5120                        match self.tokens.get(probe) {
5121                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5122                                probe += 1;
5123                            }
5124                            _ => return Ok(()),
5125                        }
5126                    }
5127                    let definer = match self.tokens.get(probe) {
5128                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5129                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5130                        _ => return Ok(()),
5131                    };
5132                    self.pos = probe + 1;
5133                    attrs.security_definer = definer;
5134                }
5135                "parallel" => {
5136                    let level = match self.tokens.get(self.pos + 1) {
5137                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5138                            FunctionParallel::Safe
5139                        }
5140                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5141                            FunctionParallel::Restricted
5142                        }
5143                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5144                            FunctionParallel::Unsafe
5145                        }
5146                        _ => return Ok(()),
5147                    };
5148                    self.pos += 2;
5149                    attrs.parallel = level;
5150                }
5151                "cost" | "rows" => {
5152                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5153                        return Ok(());
5154                    };
5155                    self.pos += 2;
5156                    if word == "cost" {
5157                        attrs.cost = Some(n);
5158                    } else {
5159                        attrs.rows = Some(n);
5160                    }
5161                }
5162                _ => return Ok(()),
5163            }
5164        }
5165    }
5166
5167    /// The numeric literal at `idx`, if there is one.
5168    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5169        match self.tokens.get(idx)? {
5170            Token::Integer(n) => Some(*n as f64),
5171            Token::Float(f) => Some(*f),
5172            Token::Numeric(t) => t.parse::<f64>().ok(),
5173            _ => None,
5174        }
5175    }
5176
5177    /// Closing `)`-terminated argument list. v7.12.4 commonly
5178    /// sees the empty `()`; typed args round-trip but the
5179    /// executor (yet) doesn't invoke them.
5180    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5181    /// it away, which is what PG does with one on a function parameter.
5182    fn skip_type_modifier(&mut self) {
5183        if !matches!(self.peek(), Token::LParen) {
5184            return;
5185        }
5186        // Only a numeric modifier — anything else is not one, and eating
5187        // it would swallow real grammar.
5188        let mut i = self.pos + 1;
5189        let mut seen_number = false;
5190        loop {
5191            match self.tokens.get(i) {
5192                Some(Token::Integer(_)) => seen_number = true,
5193                Some(Token::Comma) => {}
5194                Some(Token::RParen) => break,
5195                _ => return,
5196            }
5197            i += 1;
5198        }
5199        if !seen_number {
5200            return;
5201        }
5202        while self.pos <= i {
5203            self.advance();
5204        }
5205    }
5206
5207    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5208        let mut args: Vec<FunctionArg> = Vec::new();
5209        if matches!(self.peek(), Token::RParen) {
5210            self.advance();
5211            return Ok(args);
5212        }
5213        loop {
5214            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5215            // a reserved token; OUT / INOUT are bare idents.
5216            let mode = if matches!(self.peek(), Token::In) {
5217                self.advance();
5218                FunctionArgMode::In
5219            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5220            {
5221                self.advance();
5222                FunctionArgMode::Out
5223            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5224            {
5225                self.advance();
5226                FunctionArgMode::InOut
5227            } else {
5228                FunctionArgMode::In
5229            };
5230            // Optional name. The next token is either a name
5231            // (followed by a type ident) or the type itself.
5232            // Disambiguate by peeking ahead: if the token after
5233            // the next ident is also an ident, we treat the
5234            // first as the name.
5235            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5236            // the comma or paren, then decide. Reading at most two of
5237            // them could not spell `x double precision` at all, and
5238            // silently mis-read the bare `double precision` as a
5239            // parameter named "double" — which is what made the same
5240            // signature key two different ways.
5241            let (name, ty_token) = {
5242                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5243                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5244                    words.push(self.expect_ident_like()?);
5245                }
5246                // v7.39 (round 344) — a length / precision modifier on the
5247                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5248                // accepts it and DROPS it — `pg_get_function_arguments`
5249                // reports plain `character varying` / `numeric`, measured on
5250                // 18.4 — but SPG raised `syntax error at or near "("`,
5251                // because the modifier's parens were never consumed.
5252                self.skip_type_modifier();
5253                let whole = words.join(" ");
5254                if words.len() >= 2 && !is_multiword_type_phrase(&whole) {
5255                    (Some(words[0].clone()), words[1..].join(" "))
5256                } else {
5257                    (None, whole)
5258                }
5259            };
5260            // Type — try to map to ColumnTypeName, else Raw.
5261            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5262                Some(t) => FunctionArgType::Typed(t),
5263                None => FunctionArgType::Raw(ty_token),
5264            };
5265            args.push(FunctionArg { mode, name, ty });
5266            match self.peek() {
5267                Token::Comma => {
5268                    self.advance();
5269                    continue;
5270                }
5271                Token::RParen => {
5272                    self.advance();
5273                    return Ok(args);
5274                }
5275                other => {
5276                    return Err(self.err(alloc::format!(
5277                        "expected , or ) in function arg list, got {other:?}"
5278                    )));
5279                }
5280            }
5281        }
5282    }
5283
5284    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5285        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5286        // function whose row shape is named inline.
5287        if matches!(self.peek(), Token::Table)
5288            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5289        {
5290            self.advance(); // TABLE
5291            self.advance(); // (
5292            let mut cols: Vec<String> = Vec::new();
5293            loop {
5294                let cname = self.expect_ident_like()?;
5295                let mut ty: Vec<String> = Vec::new();
5296                loop {
5297                    match self.peek() {
5298                        Token::Comma | Token::RParen | Token::Eof => break,
5299                        _ => {}
5300                    }
5301                    match self.advance() {
5302                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5303                        other => {
5304                            if let Some(w) = unreserved_keyword_text(&other) {
5305                                ty.push(w);
5306                            }
5307                        }
5308                    }
5309                }
5310                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5311                if matches!(self.peek(), Token::Comma) {
5312                    self.advance();
5313                } else {
5314                    break;
5315                }
5316            }
5317            if matches!(self.peek(), Token::RParen) {
5318                self.advance();
5319            }
5320            return Ok(FunctionReturn::Other(alloc::format!(
5321                "TABLE({})",
5322                cols.join(", ")
5323            )));
5324        }
5325        let ident = self.expect_ident_like()?;
5326        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5327        if ident.eq_ignore_ascii_case("setof") {
5328            let inner = self.expect_ident_like()?;
5329            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5330        }
5331        if ident.eq_ignore_ascii_case("trigger") {
5332            return Ok(FunctionReturn::Trigger);
5333        }
5334        if ident.eq_ignore_ascii_case("void") {
5335            return Ok(FunctionReturn::Void);
5336        }
5337        match map_type_ident_to_column_type_name(&ident) {
5338            Some(t) => Ok(FunctionReturn::Type(t)),
5339            None => Ok(FunctionReturn::Other(ident)),
5340        }
5341    }
5342
5343    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5344        match self.peek() {
5345            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5346                self.advance();
5347                let lang = self.expect_ident_like()?;
5348                Ok(Some(lang.to_ascii_lowercase()))
5349            }
5350            _ => Ok(None),
5351        }
5352    }
5353
5354    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5355    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5356    /// (expr)]*`. The `DOMAIN` keyword has already been
5357    /// consumed. PG allows the trailing constraints in any
5358    /// order; we approximate with a small loop.
5359    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5360        let name = self.expect_ident_like()?;
5361        // Optional `AS`.
5362        if matches!(self.peek(), Token::As) {
5363            self.advance();
5364        }
5365        // v7.39 (round 259) — keep the raw type NAME when the base is not
5366        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5367        // parent domain.
5368        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5369            self.parse_type_with_implied_flags()?;
5370        let mut default: Option<Expr> = None;
5371        let mut not_null = false;
5372        let mut checks: Vec<Expr> = Vec::new();
5373        loop {
5374            match self.peek() {
5375                Token::Default => {
5376                    if default.is_some() {
5377                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5378                    }
5379                    self.advance();
5380                    default = Some(self.parse_expr(0)?);
5381                }
5382                Token::Not => {
5383                    self.advance();
5384                    if !matches!(self.peek(), Token::Null) {
5385                        return Err(self.err(alloc::format!(
5386                            "expected NULL after NOT in DOMAIN, got {:?}",
5387                            self.peek()
5388                        )));
5389                    }
5390                    self.advance();
5391                    not_null = true;
5392                }
5393                Token::Null => {
5394                    self.advance();
5395                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5396                    // is the default-nullable marker (PG accepts it),
5397                    // but AFTER a NOT NULL it is a conflict PG refuses
5398                    // (`conflicting NULL/NOT NULL constraints`,
5399                    // PG18-measured); the old arm no-opped both ways.
5400                    if not_null {
5401                        return Err(self.err(alloc::string::String::from(
5402                            "conflicting NULL/NOT NULL constraints",
5403                        )));
5404                    }
5405                }
5406                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5407                    self.advance();
5408                    if !matches!(self.peek(), Token::LParen) {
5409                        return Err(self.err(alloc::format!(
5410                            "expected '(' after CHECK in DOMAIN, got {:?}",
5411                            self.peek()
5412                        )));
5413                    }
5414                    self.advance();
5415                    let expr = self.parse_expr(0)?;
5416                    if !matches!(self.peek(), Token::RParen) {
5417                        return Err(self.err(alloc::format!(
5418                            "expected ')' after CHECK expr, got {:?}",
5419                            self.peek()
5420                        )));
5421                    }
5422                    self.advance();
5423                    checks.push(expr);
5424                }
5425                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5426                // prefix on the constraint; we drop the name and
5427                // recurse into the constraint parsing.
5428                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5429                    self.advance();
5430                    let _ = self.expect_ident_like()?;
5431                }
5432                _ => break,
5433            }
5434        }
5435        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5436            name,
5437            base_type,
5438            base_domain: base_user_ref,
5439            default,
5440            not_null,
5441            checks,
5442        }))
5443    }
5444
5445    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5446    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5447    /// consumed.
5448    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5449        let name = self.expect_ident_like()?;
5450        // Required `AS`.
5451        if !matches!(self.peek(), Token::As) {
5452            return Err(self.err(alloc::format!(
5453                "expected AS after CREATE TYPE {name:?}, got {:?}",
5454                self.peek()
5455            )));
5456        }
5457        self.advance();
5458        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5459        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5460        // on the next token: `(` = composite, ident `ENUM` = enum.
5461        if matches!(self.peek(), Token::LParen) {
5462            self.advance();
5463            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5464            let mut field_user_types: Vec<Option<String>> = Vec::new();
5465            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5466            // is legal PG (an attribute-less composite; measured — the old
5467            // e2e note claimed PG requires at least one attribute).
5468            if matches!(self.peek(), Token::RParen) {
5469                self.advance();
5470                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5471                    name,
5472                    kind: crate::ast::TypeKind::Composite {
5473                        fields,
5474                        field_user_types,
5475                    },
5476                }));
5477            }
5478            loop {
5479                let field_name = self.expect_ident_like()?;
5480                // v7.39 (round 264) — keep the raw type name when it is not
5481                // a builtin: that is how a NESTED composite field records
5482                // which composite it holds.
5483                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5484                    self.parse_type_with_implied_flags()?;
5485                fields.push((field_name, field_type));
5486                field_user_types.push(field_user_ref);
5487                if matches!(self.peek(), Token::Comma) {
5488                    self.advance();
5489                    continue;
5490                }
5491                if matches!(self.peek(), Token::RParen) {
5492                    self.advance();
5493                    break;
5494                }
5495                return Err(self.err(alloc::format!(
5496                    "expected , or ) in composite field list, got {:?}",
5497                    self.peek()
5498                )));
5499            }
5500            if fields.is_empty() {
5501                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5502            }
5503            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5504                name,
5505                kind: crate::ast::TypeKind::Composite {
5506                    fields,
5507                    field_user_types,
5508                },
5509            }));
5510        }
5511        // Required `ENUM` ident.
5512        let kind_ident = match self.peek().clone() {
5513            Token::Ident(s) | Token::QuotedIdent(s) => s,
5514            other => {
5515                return Err(self.err(alloc::format!(
5516                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5517                )));
5518            }
5519        };
5520        if !kind_ident.eq_ignore_ascii_case("enum") {
5521            return Err(self.err(alloc::format!(
5522                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5523            )));
5524        }
5525        self.advance();
5526        if !matches!(self.peek(), Token::LParen) {
5527            return Err(self.err(alloc::format!(
5528                "expected '(' after ENUM, got {:?}",
5529                self.peek()
5530            )));
5531        }
5532        self.advance();
5533        let mut labels: Vec<String> = Vec::new();
5534        loop {
5535            match self.peek().clone() {
5536                Token::String(s) => {
5537                    self.advance();
5538                    labels.push(s);
5539                }
5540                other => {
5541                    return Err(
5542                        self.err(alloc::format!("expected enum label string, got {other:?}"))
5543                    );
5544                }
5545            }
5546            if matches!(self.peek(), Token::Comma) {
5547                self.advance();
5548                continue;
5549            }
5550            if matches!(self.peek(), Token::RParen) {
5551                self.advance();
5552                break;
5553            }
5554            return Err(self.err(alloc::format!(
5555                "expected , or ) in ENUM label list, got {:?}",
5556                self.peek()
5557            )));
5558        }
5559        if labels.is_empty() {
5560            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5561        }
5562        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5563            name,
5564            kind: crate::ast::TypeKind::Enum { labels },
5565        }))
5566    }
5567
5568    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5569    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5570    /// The `CREATE MATERIALIZED VIEW` keywords have already been
5571    /// consumed.
5572    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5573        let if_not_exists = self.parse_if_not_exists();
5574        let name = self.expect_ident_like()?;
5575        let mut columns: Vec<String> = Vec::new();
5576        if matches!(self.peek(), Token::LParen) {
5577            self.advance();
5578            loop {
5579                let c = self.expect_ident_like()?;
5580                columns.push(c);
5581                if matches!(self.peek(), Token::Comma) {
5582                    self.advance();
5583                    continue;
5584                }
5585                if matches!(self.peek(), Token::RParen) {
5586                    self.advance();
5587                    break;
5588                }
5589                return Err(self.err(alloc::format!(
5590                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5591                    self.peek()
5592                )));
5593            }
5594        }
5595        if !matches!(self.peek(), Token::As) {
5596            return Err(self.err(alloc::format!(
5597                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5598                self.peek()
5599            )));
5600        }
5601        self.advance();
5602        // v7.39 (round 151) — a WITH-headed body is legal (read-only
5603        // CTEs only; the engine rejects data-modifying ones with PG's
5604        // message). A trailing `WITH [NO] DATA` can't START the body,
5605        // so WITH here heads the query.
5606        let body = if self.peek_is_with_kw() {
5607            self.advance();
5608            self.parse_nested_with_select()?
5609        } else {
5610            let body_stmt = self.parse_select_stmt()?;
5611            let Statement::Select(body) = body_stmt else {
5612                return Err(self.err(alloc::format!(
5613                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5614                )));
5615            };
5616            body
5617        };
5618        // Optional trailing `WITH [NO] DATA`.
5619        let with_data = self.parse_optional_with_data(true)?;
5620        Ok(Statement::CreateMaterializedView(
5621            crate::ast::CreateMaterializedViewStatement {
5622                temporary: false,
5623                name,
5624                if_not_exists,
5625                columns,
5626                body,
5627                with_data,
5628                as_plain_table: false,
5629            },
5630        ))
5631    }
5632
5633    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5634    /// `default_when_absent` is what to return if the tail is
5635    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5636    /// WITH DATA).
5637    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5638        let save = self.pos;
5639        // `WITH` is an Ident (not reserved in the lexer).
5640        let is_with = match self.peek() {
5641            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5642            _ => false,
5643        };
5644        if !is_with {
5645            return Ok(default_when_absent);
5646        }
5647        self.advance();
5648        // Optional `NO`.
5649        let mut with_data = true;
5650        let is_no = match self.peek() {
5651            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5652            _ => false,
5653        };
5654        if is_no {
5655            self.advance();
5656            with_data = false;
5657        }
5658        // Required `DATA` ident.
5659        let is_data = match self.peek() {
5660            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5661            _ => false,
5662        };
5663        if is_data {
5664            self.advance();
5665            Ok(with_data)
5666        } else {
5667            // Caller's WITH wasn't WITH-DATA — rewind so the outer
5668            // parser can interpret it.
5669            self.pos = save;
5670            Ok(default_when_absent)
5671        }
5672    }
5673
5674    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5675    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5676    /// All keyword prefixes have already been consumed; the flags
5677    /// say which were present.
5678    fn parse_create_view_after_keyword(
5679        &mut self,
5680        or_replace: bool,
5681        _materialized_unused: bool,
5682        temporary: bool,
5683    ) -> Result<Statement, ParseError> {
5684        let if_not_exists = self.parse_if_not_exists();
5685        let name = self.expect_ident_like()?;
5686        // Optional `(col, col, …)` rename list.
5687        let mut columns: Vec<String> = Vec::new();
5688        if matches!(self.peek(), Token::LParen) {
5689            self.advance();
5690            loop {
5691                let c = self.expect_ident_like()?;
5692                columns.push(c);
5693                if matches!(self.peek(), Token::Comma) {
5694                    self.advance();
5695                    continue;
5696                }
5697                if matches!(self.peek(), Token::RParen) {
5698                    self.advance();
5699                    break;
5700                }
5701                return Err(self.err(alloc::format!(
5702                    "expected , or ) in VIEW column list, got {:?}",
5703                    self.peek()
5704                )));
5705            }
5706        }
5707        // Required `AS`.
5708        if !matches!(self.peek(), Token::As) {
5709            return Err(self.err(alloc::format!(
5710                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5711                self.peek()
5712            )));
5713        }
5714        self.advance();
5715        // Body: a regular SELECT statement. v7.39 (round 151) — a
5716        // WITH-headed body is legal too (read-only CTEs only; the
5717        // engine rejects data-modifying ones with PG's message).
5718        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5719        // with the check-option clause, so WITH here heads the query.
5720        let body = if self.peek_is_with_kw() {
5721            self.advance();
5722            self.parse_nested_with_select()?
5723        } else {
5724            let body_stmt = self.parse_select_stmt()?;
5725            let Statement::Select(body) = body_stmt else {
5726                return Err(self.err(alloc::format!(
5727                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5728                )));
5729            };
5730            body
5731        };
5732        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5733        // The SELECT parser stops before a trailing WITH, so it lands here.
5734        let check_option = if matches!(self.peek(),
5735            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5736        {
5737            self.advance(); // WITH
5738            let opt = match self.peek() {
5739                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
5740                    self.advance();
5741                    crate::ast::ViewCheckOption::Local
5742                }
5743                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
5744                    self.advance();
5745                    crate::ast::ViewCheckOption::Cascaded
5746                }
5747                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
5748                _ => crate::ast::ViewCheckOption::Cascaded,
5749            };
5750            if !matches!(self.peek(),
5751                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
5752            {
5753                return Err(self.err(alloc::format!(
5754                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
5755                    self.peek()
5756                )));
5757            }
5758            self.advance(); // CHECK
5759            if !matches!(self.peek(),
5760                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
5761            {
5762                return Err(self.err(alloc::format!(
5763                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
5764                    self.peek()
5765                )));
5766            }
5767            self.advance(); // OPTION
5768            Some(opt)
5769        } else {
5770            None
5771        };
5772        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
5773            name,
5774            or_replace,
5775            if_not_exists,
5776            temporary,
5777            columns,
5778            body,
5779            check_option,
5780        }))
5781    }
5782
5783    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
5784    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
5785    /// consumed; `temporary` carries whether TEMPORARY was seen.
5786    fn parse_create_sequence_after_keyword(
5787        &mut self,
5788        temporary: bool,
5789    ) -> Result<Statement, ParseError> {
5790        let if_not_exists = self.parse_if_not_exists();
5791        let name = self.expect_ident_like()?;
5792        // Optional `AS data_type`.
5793        let data_type = if matches!(self.peek(), Token::As) {
5794            self.advance();
5795            Some(self.parse_sequence_data_type()?)
5796        } else {
5797            None
5798        };
5799        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
5800        Ok(Statement::CreateSequence(
5801            crate::ast::CreateSequenceStatement {
5802                name,
5803                if_not_exists,
5804                temporary,
5805                data_type,
5806                options,
5807            },
5808        ))
5809    }
5810
5811    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
5812    /// already been consumed; this is reached after `SEQUENCE`.
5813    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
5814    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5815        use crate::ast::AlterDomainAction as A;
5816        let name = self.expect_ident_like()?;
5817        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
5818        let kw = match self.peek() {
5819            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
5820            Token::Drop => alloc::string::String::from("drop"),
5821            Token::Default => alloc::string::String::from("default"),
5822            other => {
5823                return Err(self.err(alloc::format!(
5824                    "expected an ALTER DOMAIN action, got {other:?}"
5825                )));
5826            }
5827        };
5828        let action = match kw.as_str() {
5829            "add" => {
5830                self.advance();
5831                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
5832                {
5833                    self.advance();
5834                    Some(self.expect_ident_like()?)
5835                } else {
5836                    None
5837                };
5838                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
5839                    return Err(self.err(alloc::format!(
5840                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
5841                        self.peek()
5842                    )));
5843                }
5844                self.advance();
5845                if !matches!(self.peek(), Token::LParen) {
5846                    return Err(self.err("expected '(' after CHECK".into()));
5847                }
5848                self.advance();
5849                let check = self.parse_expr(0)?;
5850                if !matches!(self.peek(), Token::RParen) {
5851                    return Err(self.err("expected ')' after CHECK expression".into()));
5852                }
5853                self.advance();
5854                A::AddConstraint { name: cname, check }
5855            }
5856            "drop" => {
5857                self.advance();
5858                match self.peek() {
5859                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
5860                        self.advance();
5861                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
5862                        {
5863                            self.advance();
5864                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
5865                            {
5866                                return Err(self.err("expected EXISTS after IF".into()));
5867                            }
5868                            self.advance();
5869                            true
5870                        } else {
5871                            false
5872                        };
5873                        let cn = self.expect_ident_like()?;
5874                        A::DropConstraint {
5875                            name: cn,
5876                            if_exists,
5877                        }
5878                    }
5879                    Token::Default => {
5880                        self.advance();
5881                        A::DropDefault
5882                    }
5883                    Token::Not => {
5884                        self.advance();
5885                        if !matches!(self.peek(), Token::Null) {
5886                            return Err(self.err("expected NULL after NOT".into()));
5887                        }
5888                        self.advance();
5889                        A::DropNotNull
5890                    }
5891                    other => {
5892                        return Err(self.err(alloc::format!(
5893                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
5894                        )));
5895                    }
5896                }
5897            }
5898            "set" => {
5899                self.advance();
5900                match self.peek() {
5901                    Token::Default => {
5902                        self.advance();
5903                        A::SetDefault(self.parse_expr(0)?)
5904                    }
5905                    Token::Not => {
5906                        self.advance();
5907                        if !matches!(self.peek(), Token::Null) {
5908                            return Err(self.err("expected NULL after NOT".into()));
5909                        }
5910                        self.advance();
5911                        A::SetNotNull
5912                    }
5913                    other => {
5914                        return Err(self.err(alloc::format!(
5915                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
5916                        )));
5917                    }
5918                }
5919            }
5920            "rename" => {
5921                self.advance();
5922                if !matches!(self.peek(), Token::To) {
5923                    return Err(self.err("expected TO after RENAME".into()));
5924                }
5925                self.advance();
5926                A::RenameTo(self.expect_ident_like()?)
5927            }
5928            other => {
5929                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
5930            }
5931        };
5932        Ok(Statement::AlterDomain { name, action })
5933    }
5934
5935    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
5936        let if_exists = self.parse_if_exists();
5937        let name = self.expect_ident_like()?;
5938        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
5939        // the option list (PG allows only one or the other).
5940        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
5941            self.advance();
5942            if matches!(self.peek(), Token::To) {
5943                self.advance();
5944            } else {
5945                self.expect_keyword_ident("to")?;
5946            }
5947            let new = self.expect_ident_like()?;
5948            return Ok(Statement::AlterSequence(
5949                crate::ast::AlterSequenceStatement {
5950                    name,
5951                    if_exists,
5952                    options: crate::ast::SequenceOptions::default(),
5953                    rename_to: Some(new),
5954                },
5955            ));
5956        }
5957        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
5958        Ok(Statement::AlterSequence(
5959            crate::ast::AlterSequenceStatement {
5960                name,
5961                if_exists,
5962                options,
5963                rename_to: None,
5964            },
5965        ))
5966    }
5967
5968    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
5969        let kw = self.expect_ident_like()?;
5970        match kw.to_ascii_lowercase().as_str() {
5971            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
5972            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
5973            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
5974            other => Err(self.err(alloc::format!(
5975                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
5976            ))),
5977        }
5978    }
5979
5980    fn parse_sequence_options(
5981        &mut self,
5982        allow_restart: bool,
5983    ) -> Result<crate::ast::SequenceOptions, ParseError> {
5984        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
5985        let mut opts = SequenceOptions::default();
5986        #[allow(clippy::while_let_loop)]
5987        loop {
5988            // Match an ident; stop at any non-ident token (sentinel,
5989            // semicolon, end of statement).
5990            let kw_lc = match self.peek() {
5991                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
5992                _ => break,
5993            };
5994            match kw_lc.as_str() {
5995                "increment" => {
5996                    self.advance();
5997                    // Optional BY.
5998                    if self.peek_is_by() {
5999                        self.advance();
6000                    }
6001                    opts.increment = Some(self.expect_signed_int()?);
6002                }
6003                "minvalue" => {
6004                    self.advance();
6005                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6006                }
6007                "maxvalue" => {
6008                    self.advance();
6009                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6010                }
6011                "no" => {
6012                    self.advance();
6013                    let what = self.expect_ident_like()?;
6014                    match what.to_ascii_lowercase().as_str() {
6015                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6016                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6017                        "cycle" => opts.cycle = Some(false),
6018                        other => {
6019                            return Err(self.err(alloc::format!(
6020                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6021                            )));
6022                        }
6023                    }
6024                }
6025                "start" => {
6026                    self.advance();
6027                    // Optional WITH.
6028                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6029                        if s.eq_ignore_ascii_case("with"))
6030                    {
6031                        self.advance();
6032                    }
6033                    opts.start = Some(self.expect_signed_int()?);
6034                }
6035                "restart" if allow_restart => {
6036                    self.advance();
6037                    // Optional WITH n; bare RESTART means restart at START.
6038                    let mut with_val: Option<i64> = None;
6039                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6040                        if s.eq_ignore_ascii_case("with"))
6041                    {
6042                        self.advance();
6043                        with_val = Some(self.expect_signed_int()?);
6044                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6045                        with_val = Some(self.expect_signed_int()?);
6046                    }
6047                    opts.restart = Some(with_val);
6048                }
6049                "cache" => {
6050                    self.advance();
6051                    opts.cache = Some(self.expect_signed_int()?);
6052                }
6053                "cycle" => {
6054                    self.advance();
6055                    opts.cycle = Some(true);
6056                }
6057                "owned" => {
6058                    self.advance();
6059                    match self.peek() {
6060                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6061                            self.advance();
6062                        }
6063                        other => {
6064                            return Err(
6065                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6066                            );
6067                        }
6068                    }
6069                    // OWNED BY {NONE | tab.col}. Read just one ident
6070                    // (NOT expect_ident_like which would auto-strip
6071                    // a schema prefix and consume the `.col` we need).
6072                    let first = match self.advance() {
6073                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6074                        other => {
6075                            return Err(self.err(alloc::format!(
6076                                "expected identifier or NONE after OWNED BY, got {other:?}"
6077                            )));
6078                        }
6079                    };
6080                    if first.eq_ignore_ascii_case("none") {
6081                        opts.owned_by = Some(SequenceOwnedBy::None);
6082                    } else if matches!(self.peek(), Token::Dot) {
6083                        self.advance();
6084                        let second = match self.advance() {
6085                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6086                            other => {
6087                                return Err(self.err(alloc::format!(
6088                                    "expected column name after OWNED BY {first}., got {other:?}"
6089                                )));
6090                            }
6091                        };
6092                        // v7.17 dump-compat fix — pg_dump emits
6093                        // OWNED BY clauses as
6094                        // `schema.table.column` (three segments).
6095                        // If a third `.<ident>` follows, treat the
6096                        // first ident as schema (drop it; SPG is
6097                        // single-schema) and the middle / last
6098                        // pair as table.column. Otherwise it's
6099                        // the two-segment form table.column.
6100                        if matches!(self.peek(), Token::Dot) {
6101                            self.advance();
6102                            let third = match self.advance() {
6103                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6104                                other => {
6105                                    return Err(self.err(alloc::format!(
6106                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6107                                    )));
6108                                }
6109                            };
6110                            let _ = first; // schema prefix discarded
6111                            opts.owned_by = Some(SequenceOwnedBy::Column {
6112                                table: second,
6113                                column: third,
6114                            });
6115                        } else {
6116                            opts.owned_by = Some(SequenceOwnedBy::Column {
6117                                table: first,
6118                                column: second,
6119                            });
6120                        }
6121                    } else {
6122                        return Err(self.err(alloc::format!(
6123                            "expected table.column or NONE after OWNED BY, got {first:?}"
6124                        )));
6125                    }
6126                }
6127                _ => break,
6128            }
6129        }
6130        Ok(opts)
6131    }
6132
6133    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6134        let neg = if matches!(self.peek(), Token::Minus) {
6135            self.advance();
6136            true
6137        } else {
6138            false
6139        };
6140        match self.peek() {
6141            Token::Integer(n) => {
6142                let v = *n;
6143                self.advance();
6144                Ok(if neg { -v } else { v })
6145            }
6146            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6147        }
6148    }
6149
6150    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6151    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6152    /// clause is fully accepted and discarded — SPG always runs
6153    /// constraint checks immediately (single-writer model). The
6154    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6155    /// in either order (per the SQL spec they're independent),
6156    /// though pg_dump always emits them in the canonical
6157    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6158    /// Stops at the first token that isn't part of the clause.
6159    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6160        self.consume_deferrable_clauses_timed().map(|_| ())
6161    }
6162
6163    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6164    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6165    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6166    /// NOT DEFERRABLE and a circular-FK migration could not load.
6167    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6168        let mut deferrable = false;
6169        let mut initially_deferred = false;
6170        loop {
6171            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6172            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6173                self.advance();
6174                deferrable = true;
6175                if self.consume_optional_initially_clause()? {
6176                    initially_deferred = true;
6177                }
6178                continue;
6179            }
6180            // `NOT DEFERRABLE` — already worked pre-3.1.
6181            if matches!(self.peek(), Token::Not) {
6182                let look = self.tokens.get(self.pos + 1);
6183                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6184                    self.advance(); // NOT
6185                    self.advance(); // DEFERRABLE
6186                    deferrable = false;
6187                    initially_deferred = false;
6188                    let _ = self.consume_optional_initially_clause()?;
6189                    continue;
6190                }
6191                break;
6192            }
6193            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6194            // accepts this without a leading [NOT] DEFERRABLE
6195            // (the timing keyword alone). pg_dump occasionally
6196            // emits it on FK constraints that inherit timing.
6197            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6198                if self.consume_optional_initially_clause()? {
6199                    initially_deferred = true;
6200                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6201                    deferrable = true;
6202                }
6203                continue;
6204            }
6205            break;
6206        }
6207        Ok((deferrable, initially_deferred))
6208    }
6209
6210    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6211    /// next token is `INITIALLY`, consume it plus the required
6212    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6213    /// Returns true when the timing seen was `DEFERRED`.
6214    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6215        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6216            return Ok(false);
6217        }
6218        self.advance(); // INITIALLY
6219        match self.advance() {
6220            Token::Ident(s)
6221                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6222            {
6223                Ok(s.eq_ignore_ascii_case("deferred"))
6224            }
6225            other => Err(self.err(alloc::format!(
6226                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6227            ))),
6228        }
6229    }
6230
6231    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6232    /// in its entirety so the parser returns Empty without
6233    /// touching the runtime. The CREATE+PROCEDURE keywords are
6234    /// already consumed; this swallows everything from the
6235    /// procedure name through the matching `END`, including
6236    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6237    /// (DELIMITER `//` makes the script splitter forward the
6238    /// whole block as one statement), `@var` session-variable
6239    /// references, and the trailing terminator.
6240    ///
6241    /// Tracks nesting depth so:
6242    ///   BEGIN
6243    ///     IF cond THEN
6244    ///       BEGIN ... END;
6245    ///     END IF;
6246    ///   END
6247    /// terminates at the outer END.
6248    fn consume_mysql_routine_body(&mut self) {
6249        // Outer skeleton: name, (...), optional clauses, BEGIN
6250        // <body> END [;]. Scan for the first BEGIN — anything
6251        // before it is signature decoration we don't care about.
6252        // Once inside BEGIN, count up on BEGIN, down on END.
6253        let mut depth: i32 = 0;
6254        let mut started = false;
6255        loop {
6256            match self.peek().clone() {
6257                Token::Begin => {
6258                    self.advance();
6259                    depth += 1;
6260                    started = true;
6261                }
6262                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6263                    self.advance();
6264                    if started {
6265                        depth -= 1;
6266                        if depth <= 0 {
6267                            // Optional trailing ident (`END IF`,
6268                            // `END LOOP`, `END WHILE`, `END CASE`,
6269                            // `END label_name`) — eat the next
6270                            // ident if present so we don't
6271                            // mistake `END IF;` for the outer
6272                            // close.
6273                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6274                                // If the next token is one of the
6275                                // PL/SQL block-closer keywords,
6276                                // the END belongs to an inner
6277                                // block; bump depth back up.
6278                                let is_inner_close = matches!(
6279                                    self.peek(),
6280                                    Token::Ident(s) | Token::QuotedIdent(s)
6281                                        if matches!(
6282                                            s.to_ascii_lowercase().as_str(),
6283                                            "if" | "loop" | "while" | "case" | "repeat"
6284                                        )
6285                                );
6286                                if is_inner_close {
6287                                    self.advance();
6288                                    depth += 1;
6289                                    continue;
6290                                }
6291                            }
6292                            // Eat optional trailing `;`.
6293                            if matches!(self.peek(), Token::Semicolon) {
6294                                self.advance();
6295                            }
6296                            return;
6297                        }
6298                    }
6299                }
6300                Token::Eof => return,
6301                _ => {
6302                    self.advance();
6303                }
6304            }
6305        }
6306    }
6307
6308    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6309    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6310    ///
6311    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6312    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6313    ///   ident, or `ident @ ident-or-quoted-string` host form)
6314    /// * `SQL SECURITY {DEFINER|INVOKER}`
6315    ///
6316    /// Each clause may appear at most once but in any order.
6317    /// The hints are pure planner / permission metadata that
6318    /// SPG's view-rewrite engine handles uniformly; we accept
6319    /// and discard. Returns `Ok(())` once a non-clause token is
6320    /// peeked (the caller then checks for the `VIEW` keyword).
6321    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6322        loop {
6323            match self.peek().clone() {
6324                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6325                    self.advance(); // ALGORITHM
6326                    // Optional `=`. MySQL spec requires it but be
6327                    // generous.
6328                    if matches!(self.peek(), Token::Eq) {
6329                        self.advance();
6330                    }
6331                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6332                    // bare ident; unknown values still parse so
6333                    // future MySQL versions don't break.
6334                    if matches!(
6335                        self.peek(),
6336                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6337                    ) {
6338                        self.advance();
6339                    }
6340                }
6341                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6342                    self.advance(); // DEFINER
6343                    if matches!(self.peek(), Token::Eq) {
6344                        self.advance();
6345                    }
6346                    // User: quoted string, ident, OR ident @ host
6347                    // (host may itself be quoted or bare).
6348                    match self.peek().clone() {
6349                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6350                            self.advance();
6351                            // Optional `@host`.
6352                            if matches!(self.peek(), Token::At) {
6353                                self.advance();
6354                                if matches!(
6355                                    self.peek(),
6356                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6357                                ) {
6358                                    self.advance();
6359                                }
6360                            }
6361                        }
6362                        _ => {}
6363                    }
6364                }
6365                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6366                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6367                    // when followed by SECURITY — the dispatcher must
6368                    // not consume a bare `SQL` token (it's not a
6369                    // legal CREATE prefix on its own).
6370                    let save = self.pos;
6371                    self.advance(); // SQL
6372                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6373                        if s2.eq_ignore_ascii_case("security"))
6374                    {
6375                        self.advance(); // SECURITY
6376                        // DEFINER / INVOKER trailing ident.
6377                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6378                            self.advance();
6379                        }
6380                    } else {
6381                        // Not a SQL SECURITY clause — roll back and
6382                        // bail; the caller will error out cleanly.
6383                        self.pos = save;
6384                        return Ok(());
6385                    }
6386                }
6387                _ => return Ok(()),
6388            }
6389        }
6390    }
6391
6392    fn parse_if_not_exists(&mut self) -> bool {
6393        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6394        {
6395            let save = self.pos;
6396            self.advance();
6397            if matches!(self.peek(), Token::Not) {
6398                self.advance();
6399                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6400                {
6401                    self.advance();
6402                    return true;
6403                }
6404            }
6405            self.pos = save;
6406        }
6407        false
6408    }
6409
6410    fn parse_if_exists(&mut self) -> bool {
6411        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6412        {
6413            let save = self.pos;
6414            self.advance();
6415            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6416            {
6417                self.advance();
6418                return true;
6419            }
6420            self.pos = save;
6421        }
6422        false
6423    }
6424
6425    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6426    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6427    /// been consumed.
6428    fn parse_create_trigger_after_keyword(
6429        &mut self,
6430        or_replace: bool,
6431    ) -> Result<Statement, ParseError> {
6432        let name = self.expect_ident_like()?;
6433        let timing = {
6434            let ident = self.expect_ident_like()?;
6435            if ident.eq_ignore_ascii_case("before") {
6436                TriggerTiming::Before
6437            } else if ident.eq_ignore_ascii_case("after") {
6438                TriggerTiming::After
6439            } else if ident.eq_ignore_ascii_case("instead") {
6440                let next = self.expect_ident_like()?;
6441                if !next.eq_ignore_ascii_case("of") {
6442                    return Err(self.err(alloc::format!(
6443                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6444                    )));
6445                }
6446                TriggerTiming::InsteadOf
6447            } else {
6448                return Err(self.err(alloc::format!(
6449                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6450                )));
6451            }
6452        };
6453        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6454        // OR is a reserved keyword token (Token::Or), not an Ident.
6455        // v7.13.0 — after an UPDATE event we may optionally see
6456        // `OF col, col, …` (mailrs round-5 G7). Columns are
6457        // captured into `update_columns` once across the whole
6458        // events list; multiple `UPDATE OF` clauses are rejected.
6459        let mut events: Vec<TriggerEvent> = Vec::new();
6460        let mut update_columns: Vec<String> = Vec::new();
6461        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6462        events.push(first_ev);
6463        if !first_cols.is_empty() {
6464            update_columns = first_cols;
6465        }
6466        while matches!(self.peek(), Token::Or) {
6467            self.advance();
6468            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6469            events.push(ev);
6470            if !cols.is_empty() {
6471                if !update_columns.is_empty() {
6472                    return Err(
6473                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6474                    );
6475                }
6476                update_columns = cols;
6477            }
6478        }
6479        // ON <table>
6480        let tok = self.peek();
6481        let Token::On = tok else {
6482            return Err(self.err(alloc::format!(
6483                "expected ON after trigger events, got {tok:?}"
6484            )));
6485        };
6486        self.advance();
6487        let table = self.expect_ident_like()?;
6488        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6489        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6490        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6491        // the trigger as a plain AFTER trigger (correct for every non-deferred
6492        // use; deferral timing is not yet honoured).
6493        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6494            if s.eq_ignore_ascii_case("from"))
6495        {
6496            self.advance();
6497            let _reftable = self.expect_ident_like()?;
6498        }
6499        self.consume_optional_deferrable_clauses()?;
6500        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6501        // keyword (Token::For); EACH / ROW / STATEMENT are bare
6502        // idents.
6503        if !matches!(self.peek(), Token::For) {
6504            return Err(self.err(alloc::format!(
6505                "expected FOR EACH ROW / STATEMENT, got {:?}",
6506                self.peek()
6507            )));
6508        }
6509        self.advance();
6510        let for_each = {
6511            let e = self.expect_ident_like()?;
6512            if !e.eq_ignore_ascii_case("each") {
6513                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6514            }
6515            let unit = self.expect_ident_like()?;
6516            if unit.eq_ignore_ascii_case("row") {
6517                TriggerForEach::Row
6518            } else if unit.eq_ignore_ascii_case("statement") {
6519                TriggerForEach::Statement
6520            } else {
6521                return Err(self.err(alloc::format!(
6522                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6523                )));
6524            }
6525        };
6526        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6527        let when_condition = if matches!(self.peek(),
6528            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6529        {
6530            self.advance();
6531            Some(self.parse_paren_expr("WHEN")?)
6532        } else {
6533            None
6534        };
6535        // EXECUTE FUNCTION/PROCEDURE name(...)
6536        let exec = self.expect_ident_like()?;
6537        if !exec.eq_ignore_ascii_case("execute") {
6538            return Err(self.err(alloc::format!(
6539                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6540            )));
6541        }
6542        let fn_or_proc = self.expect_ident_like()?;
6543        if !(fn_or_proc.eq_ignore_ascii_case("function")
6544            || fn_or_proc.eq_ignore_ascii_case("procedure"))
6545        {
6546            return Err(self.err(alloc::format!(
6547                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6548            )));
6549        }
6550        let function = self.expect_ident_like()?;
6551        // Optional empty arg list `()`.
6552        if matches!(self.peek(), Token::LParen) {
6553            self.advance();
6554            if !matches!(self.peek(), Token::RParen) {
6555                return Err(self.err(alloc::format!(
6556                    "v7.12.4 trigger function calls take no args; got {:?}",
6557                    self.peek()
6558                )));
6559            }
6560            self.advance();
6561        }
6562        Ok(Statement::CreateTrigger(CreateTriggerStatement {
6563            name,
6564            or_replace,
6565            timing,
6566            events,
6567            table,
6568            for_each,
6569            function,
6570            update_columns,
6571            when_condition,
6572        }))
6573    }
6574
6575    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6576    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6577    fn parse_create_rule_after_keyword(
6578        &mut self,
6579        or_replace: bool,
6580    ) -> Result<Statement, ParseError> {
6581        let name = self.expect_ident_like()?;
6582        if !matches!(self.peek(), Token::As) {
6583            return Err(self.err(alloc::format!(
6584                "expected AS in CREATE RULE, got {:?}",
6585                self.peek()
6586            )));
6587        }
6588        self.advance();
6589        if !matches!(self.peek(), Token::On) {
6590            return Err(self.err(alloc::format!(
6591                "expected ON in CREATE RULE, got {:?}",
6592                self.peek()
6593            )));
6594        }
6595        self.advance();
6596        let event = self.parse_rule_event()?;
6597        if !matches!(self.peek(), Token::To)
6598            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6599        {
6600            return Err(self.err(alloc::format!(
6601                "expected TO after rule event, got {:?}",
6602                self.peek()
6603            )));
6604        }
6605        self.advance();
6606        let table = self.expect_ident_like()?;
6607        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6608        let when_condition = if matches!(self.peek(), Token::Where) {
6609            self.advance();
6610            Some(self.parse_expr(0)?)
6611        } else {
6612            None
6613        };
6614        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6615        {
6616            return Err(self.err(alloc::format!(
6617                "expected DO in CREATE RULE, got {:?}",
6618                self.peek()
6619            )));
6620        }
6621        self.advance();
6622        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6623        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6624        {
6625            self.advance();
6626            true
6627        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6628            self.advance();
6629            false
6630        } else {
6631            false
6632        };
6633        // `NOTHING` | `( cmd; … )` | `cmd`.
6634        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6635        {
6636            self.advance();
6637            Vec::new()
6638        } else if matches!(self.peek(), Token::LParen) {
6639            self.advance();
6640            let mut cmds = Vec::new();
6641            loop {
6642                cmds.push(self.parse_one_statement()?);
6643                if matches!(self.peek(), Token::Semicolon) {
6644                    self.advance();
6645                    if matches!(self.peek(), Token::RParen) {
6646                        break;
6647                    }
6648                    continue;
6649                }
6650                break;
6651            }
6652            if !matches!(self.peek(), Token::RParen) {
6653                return Err(self.err(alloc::format!(
6654                    "expected ) closing the CREATE RULE command list, got {:?}",
6655                    self.peek()
6656                )));
6657            }
6658            self.advance();
6659            cmds
6660        } else {
6661            alloc::vec![self.parse_one_statement()?]
6662        };
6663        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6664            name,
6665            or_replace,
6666            event,
6667            table,
6668            instead,
6669            when_condition,
6670            commands,
6671        }))
6672    }
6673
6674    /// v7.39 (round 139) — a rule event keyword → uppercase string.
6675    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6676        if matches!(self.peek(), Token::Insert) {
6677            self.advance();
6678            return Ok(alloc::string::String::from("INSERT"));
6679        }
6680        if matches!(self.peek(), Token::Select) {
6681            self.advance();
6682            return Ok(alloc::string::String::from("SELECT"));
6683        }
6684        match self.peek() {
6685            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6686                self.advance();
6687                Ok(alloc::string::String::from("UPDATE"))
6688            }
6689            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6690                self.advance();
6691                Ok(alloc::string::String::from("DELETE"))
6692            }
6693            other => Err(self.err(alloc::format!(
6694                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6695            ))),
6696        }
6697    }
6698
6699    /// v7.13.0 — parse one trigger event, then optionally consume
6700    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6701    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6702    fn parse_trigger_event_with_optional_of(
6703        &mut self,
6704    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6705        let ev = self.parse_trigger_event()?;
6706        if !matches!(ev, TriggerEvent::Update) {
6707            return Ok((ev, Vec::new()));
6708        }
6709        // `OF` is a bare ident.
6710        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6711            return Ok((ev, Vec::new()));
6712        }
6713        self.advance(); // OF
6714        let mut cols: Vec<String> = Vec::new();
6715        loop {
6716            cols.push(self.expect_ident_like()?);
6717            if matches!(self.peek(), Token::Comma) {
6718                self.advance();
6719                continue;
6720            }
6721            break;
6722        }
6723        if cols.is_empty() {
6724            return Err(
6725                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6726            );
6727        }
6728        Ok((ev, cols))
6729    }
6730
6731    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6732    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6733    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6734    /// inside the body.
6735    /// Called by [`parse_plpgsql_body`] after the body's tokens
6736    /// have been lexed into this temporary parser.
6737    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
6738        // v7.12.6 — optional DECLARE prelude.
6739        let declarations = if matches!(
6740            self.peek(),
6741            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
6742        ) {
6743            self.advance();
6744            self.parse_plpgsql_declare_block()?
6745        } else {
6746            Vec::new()
6747        };
6748        // BEGIN keyword (PL/pgSQL — distinct from the SQL
6749        // `BEGIN` transaction-start, but we can reuse the
6750        // reserved Token::Begin since the body is a separate
6751        // lex/parse context).
6752        if !matches!(self.peek(), Token::Begin) {
6753            return Err(self.err(alloc::format!(
6754                "expected BEGIN at start of plpgsql block, got {:?}",
6755                self.peek()
6756            )));
6757        }
6758        self.advance();
6759        let statements = self.parse_plpgsql_stmt_list_until_end()?;
6760        // v7.37.20 (20.10) — optional EXCEPTION clause between the
6761        // body's last statement and the trailing END. When present
6762        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
6763        // arms terminated by END.
6764        let exception_handlers = if matches!(
6765            self.peek(),
6766            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
6767        ) {
6768            self.advance();
6769            self.parse_plpgsql_exception_handlers()?
6770        } else {
6771            Vec::new()
6772        };
6773        Ok(PlPgSqlBlock {
6774            declarations,
6775            statements,
6776            exception_handlers,
6777        })
6778    }
6779
6780    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
6781    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
6782    fn parse_plpgsql_exception_handlers(
6783        &mut self,
6784    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
6785        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
6786        loop {
6787            // Stop at END — the block-level trailing END LOOP / END;
6788            // is handled by the caller.
6789            if matches!(
6790                self.peek(),
6791                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
6792            ) {
6793                return Ok(out);
6794            }
6795            // WHEN <cond> [OR <cond>]* THEN <body>
6796            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6797            {
6798                return Err(self.err(alloc::format!(
6799                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
6800                    self.peek()
6801                )));
6802            }
6803            self.advance();
6804            let mut conditions: Vec<String> = Vec::new();
6805            conditions.push(self.expect_ident_like()?);
6806            while matches!(self.peek(), Token::Or) {
6807                self.advance();
6808                conditions.push(self.expect_ident_like()?);
6809            }
6810            let then_kw = self.expect_ident_like()?;
6811            if !then_kw.eq_ignore_ascii_case("then") {
6812                return Err(self.err(alloc::format!(
6813                    "expected THEN after WHEN condition list, got {then_kw:?}"
6814                )));
6815            }
6816            let body = self.parse_plpgsql_stmt_list_until_end()?;
6817            out.push(crate::ast::ExceptionHandler { conditions, body });
6818        }
6819    }
6820
6821    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
6822    /// prelude. Caller has already consumed `DECLARE`. We stop
6823    /// reading entries when we hit `BEGIN`.
6824    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
6825        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
6826        loop {
6827            if matches!(self.peek(), Token::Begin) {
6828                return Ok(out);
6829            }
6830            let name = self.expect_ident_like()?;
6831            // v7.37.20 (20.7) — type inference: if the next token is
6832            // `:=` or `=` (no explicit type), infer from the default
6833            // expression. Otherwise the ident that follows is the
6834            // declared type.
6835            //
6836            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
6837            // (PG-standard). SPG parse-accepts and treats identically
6838            // to inference — the eventual runtime value determines
6839            // the local's type, which is faithful to how SPG handles
6840            // untyped locals today (see 20.7). Full compile-time
6841            // catalog lookup queues with v7.40 PL/pgSQL epic.
6842            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
6843                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
6844                // downstream declaration walker to type the local by
6845                // the runtime type of the default expression.
6846                FunctionArgType::Raw("_infer_".into())
6847            } else {
6848                let ty_token = self.expect_ident_like()?;
6849                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
6850                // consume optional `.<ident>` qualifier + `%<KW>`
6851                // suffix. Both qualifier and suffix map to _infer_.
6852                if matches!(self.peek(), Token::Dot) {
6853                    self.advance();
6854                    let _ = self.expect_ident_like()?;
6855                }
6856                if matches!(self.peek(), Token::Percent) {
6857                    self.advance();
6858                    // Consume the trailing TYPE / ROWTYPE ident.
6859                    let _ = self.expect_ident_like()?;
6860                    FunctionArgType::Raw("_infer_".into())
6861                } else {
6862                    match map_type_ident_to_column_type_name(&ty_token) {
6863                        Some(t) => FunctionArgType::Typed(t),
6864                        None => FunctionArgType::Raw(ty_token),
6865                    }
6866                }
6867            };
6868            let default = match self.peek() {
6869                Token::ColonEq => {
6870                    self.advance();
6871                    Some(self.parse_expr(0)?)
6872                }
6873                Token::Eq => {
6874                    // PL/pgSQL also accepts `=` for the
6875                    // DECLARE default (PG treats them the same
6876                    // in this position).
6877                    self.advance();
6878                    Some(self.parse_expr(0)?)
6879                }
6880                _ => None,
6881            };
6882            // Mandatory `;` between declarations.
6883            if !matches!(self.peek(), Token::Semicolon) {
6884                return Err(self.err(alloc::format!(
6885                    "expected ; after DECLARE entry for {name:?}, got {:?}",
6886                    self.peek()
6887                )));
6888            }
6889            self.advance();
6890            out.push(PlPgSqlDeclare { name, ty, default });
6891        }
6892    }
6893
6894    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
6895    /// the terminating `END;` (or `END IF;` etc — handled by the
6896    /// per-construct sub-parsers). Used by both the outer block
6897    /// and the IF/ELSE branch bodies.
6898    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
6899        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
6900        loop {
6901            // Allow trailing semicolons + END.
6902            while matches!(self.peek(), Token::Semicolon) {
6903                self.advance();
6904            }
6905            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
6906            if matches!(
6907                self.peek(),
6908                Token::Ident(s) | Token::QuotedIdent(s)
6909                    if s.eq_ignore_ascii_case("end")
6910                        || s.eq_ignore_ascii_case("else")
6911                        || s.eq_ignore_ascii_case("elsif")
6912                        || s.eq_ignore_ascii_case("elseif")
6913                        || s.eq_ignore_ascii_case("exception")
6914                        || s.eq_ignore_ascii_case("when")
6915            ) {
6916                return Ok(statements);
6917            }
6918            // Otherwise: one statement, then expect `;` or
6919            // a block-terminator keyword.
6920            let stmt = self.parse_plpgsql_stmt()?;
6921            statements.push(stmt);
6922            match self.peek() {
6923                Token::Semicolon => {
6924                    self.advance();
6925                }
6926                Token::Ident(s) | Token::QuotedIdent(s)
6927                    if s.eq_ignore_ascii_case("end")
6928                        || s.eq_ignore_ascii_case("else")
6929                        || s.eq_ignore_ascii_case("elsif")
6930                        || s.eq_ignore_ascii_case("elseif")
6931                        || s.eq_ignore_ascii_case("exception")
6932                        || s.eq_ignore_ascii_case("when") =>
6933                {
6934                    // Final statement of the block without `;`.
6935                }
6936                other => {
6937                    return Err(self.err(alloc::format!(
6938                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
6939                    )));
6940                }
6941            }
6942        }
6943    }
6944
6945    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
6946        // RETURN keyword?
6947        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
6948        {
6949            self.advance();
6950            return self.parse_plpgsql_return();
6951        }
6952        // v7.12.6 — IF block.
6953        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6954        {
6955            self.advance();
6956            return self.parse_plpgsql_if();
6957        }
6958        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
6959        // Detected by peeking that token pos+3 is Ident("execute").
6960        if matches!(self.peek(), Token::For)
6961            && matches!(
6962                self.tokens.get(self.pos + 1),
6963                Some(Token::Ident(_) | Token::QuotedIdent(_))
6964            )
6965            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
6966            && matches!(
6967                self.tokens.get(self.pos + 3),
6968                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
6969            )
6970        {
6971            self.advance(); // FOR
6972            let var = self.expect_ident_like()?;
6973            self.advance(); // IN
6974            self.advance(); // EXECUTE
6975            // Prescan for LOOP at paren depth 0 so parse_expr stops
6976            // before the LOOP keyword (same trick as the bare-SELECT
6977            // ForQuery arm).
6978            let mut depth: i32 = 0;
6979            let mut loop_pos: Option<usize> = None;
6980            let mut scan = self.pos;
6981            while scan < self.tokens.len() {
6982                match self.tokens.get(scan) {
6983                    Some(Token::LParen) => depth += 1,
6984                    Some(Token::RParen) => depth -= 1,
6985                    Some(Token::Ident(s) | Token::QuotedIdent(s))
6986                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
6987                    {
6988                        loop_pos = Some(scan);
6989                        break;
6990                    }
6991                    _ => {}
6992                }
6993                scan += 1;
6994            }
6995            let loop_pos = loop_pos.ok_or_else(|| {
6996                self.err(alloc::format!(
6997                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
6998                ))
6999            })?;
7000            let saved_loop = self.tokens[loop_pos].clone();
7001            self.tokens[loop_pos] = Token::Semicolon;
7002            let expr_result = self.parse_expr(0);
7003            self.tokens[loop_pos] = saved_loop;
7004            let sql_expr = expr_result?;
7005            let loop_kw = self.expect_ident_like()?;
7006            if !loop_kw.eq_ignore_ascii_case("loop") {
7007                return Err(self.err(alloc::format!(
7008                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7009                )));
7010            }
7011            let body = self.parse_plpgsql_stmt_list_until_end()?;
7012            let end_kw = self.expect_ident_like()?;
7013            if !end_kw.eq_ignore_ascii_case("end") {
7014                return Err(self.err(alloc::format!(
7015                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7016                )));
7017            }
7018            let loop_kw2 = self.expect_ident_like()?;
7019            if !loop_kw2.eq_ignore_ascii_case("loop") {
7020                return Err(self.err(alloc::format!(
7021                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7022                )));
7023            }
7024            return Ok(PlPgSqlStmt::ForExecute {
7025                var,
7026                sql_expr,
7027                body,
7028            });
7029        }
7030        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7031        //
7032        // Two syntactic forms:
7033        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7034        //   FOR var IN (SELECT ...) LOOP ...
7035        //
7036        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7037        // the trailing `LOOP` keyword as a table alias, we prescan
7038        // forward to find LOOP at paren depth 0, splice a fake
7039        // Semicolon at that position (so SELECT parses cleanly),
7040        // then re-splice LOOP back in.
7041        //
7042        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7043        // LOOP directly — no scan required.
7044        if matches!(self.peek(), Token::For)
7045            && matches!(
7046                self.tokens.get(self.pos + 1),
7047                Some(Token::Ident(_) | Token::QuotedIdent(_))
7048            )
7049            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7050            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7051                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7052        {
7053            self.advance(); // FOR
7054            let var = self.expect_ident_like()?;
7055            // IN
7056            self.advance();
7057            let query = if matches!(self.peek(), Token::LParen) {
7058                // Paren-wrapped SELECT.
7059                self.advance();
7060                let inner = self.parse_select_stmt()?;
7061                let Statement::Select(q) = inner else {
7062                    return Err(self.err(alloc::format!(
7063                        "expected SELECT inside (…), got {:?}",
7064                        self.peek()
7065                    )));
7066                };
7067                if !matches!(self.peek(), Token::RParen) {
7068                    return Err(self.err(alloc::format!(
7069                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7070                        self.peek()
7071                    )));
7072                }
7073                self.advance();
7074                q
7075            } else {
7076                // Bare SELECT: prescan to find the LOOP boundary.
7077                let mut depth: i32 = 0;
7078                let mut loop_pos: Option<usize> = None;
7079                let mut scan = self.pos;
7080                while scan < self.tokens.len() {
7081                    match self.tokens.get(scan) {
7082                        Some(Token::LParen) => depth += 1,
7083                        Some(Token::RParen) => depth -= 1,
7084                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7085                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7086                        {
7087                            loop_pos = Some(scan);
7088                            break;
7089                        }
7090                        _ => {}
7091                    }
7092                    scan += 1;
7093                }
7094                let loop_pos = loop_pos.ok_or_else(|| {
7095                    self.err(alloc::format!(
7096                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7097                    ))
7098                })?;
7099                // Swap the LOOP token with a synthetic Semicolon so
7100                // parse_select_stmt stops there, then restore afterward.
7101                let saved_loop = self.tokens[loop_pos].clone();
7102                self.tokens[loop_pos] = Token::Semicolon;
7103                let parse_result = self.parse_select_stmt();
7104                self.tokens[loop_pos] = saved_loop;
7105                let inner = parse_result?;
7106                let Statement::Select(q) = inner else {
7107                    return Err(self.err(alloc::format!(
7108                        "expected SELECT after FOR <var> IN, got {:?}",
7109                        self.peek()
7110                    )));
7111                };
7112                q
7113            };
7114            let loop_kw = self.expect_ident_like()?;
7115            if !loop_kw.eq_ignore_ascii_case("loop") {
7116                return Err(self.err(alloc::format!(
7117                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7118                )));
7119            }
7120            let body = self.parse_plpgsql_stmt_list_until_end()?;
7121            let end_kw = self.expect_ident_like()?;
7122            if !end_kw.eq_ignore_ascii_case("end") {
7123                return Err(self.err(alloc::format!(
7124                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7125                )));
7126            }
7127            let loop_kw2 = self.expect_ident_like()?;
7128            if !loop_kw2.eq_ignore_ascii_case("loop") {
7129                return Err(self.err(alloc::format!(
7130                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7131                )));
7132            }
7133            return Ok(PlPgSqlStmt::ForQuery {
7134                var,
7135                query: Box::new(query),
7136                body,
7137            });
7138        }
7139        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7140        // FOR is a reserved keyword token (Token::For).
7141        if matches!(self.peek(), Token::For)
7142            && matches!(
7143                self.tokens.get(self.pos + 1),
7144                Some(Token::Ident(_) | Token::QuotedIdent(_))
7145            )
7146            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7147        {
7148            self.advance(); // FOR
7149            let var = self.expect_ident_like()?;
7150            if !matches!(self.peek(), Token::In) {
7151                return Err(self.err(alloc::format!(
7152                    "expected IN after FOR <var>, got {:?}",
7153                    self.peek()
7154                )));
7155            }
7156            self.advance();
7157            let reverse = matches!(
7158                self.peek(),
7159                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7160            );
7161            if reverse {
7162                self.advance();
7163            }
7164            let start = self.parse_expr(0)?;
7165            if !matches!(self.peek(), Token::DotDot) {
7166                return Err(self.err(alloc::format!(
7167                    "expected '..' between FOR loop bounds, got {:?}",
7168                    self.peek()
7169                )));
7170            }
7171            self.advance();
7172            let end = self.parse_expr(0)?;
7173            let loop_kw = self.expect_ident_like()?;
7174            if !loop_kw.eq_ignore_ascii_case("loop") {
7175                return Err(self.err(alloc::format!(
7176                    "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7177                )));
7178            }
7179            let body = self.parse_plpgsql_stmt_list_until_end()?;
7180            let end_kw = self.expect_ident_like()?;
7181            if !end_kw.eq_ignore_ascii_case("end") {
7182                return Err(self.err(alloc::format!(
7183                    "expected END LOOP after FOR body, got {end_kw:?}"
7184                )));
7185            }
7186            let loop_kw2 = self.expect_ident_like()?;
7187            if !loop_kw2.eq_ignore_ascii_case("loop") {
7188                return Err(self.err(alloc::format!(
7189                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7190                )));
7191            }
7192            return Ok(PlPgSqlStmt::ForRange {
7193                var,
7194                start,
7195                end,
7196                reverse,
7197                body,
7198            });
7199        }
7200        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7201        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7202        {
7203            self.advance();
7204            let body = self.parse_plpgsql_stmt_list_until_end()?;
7205            let end_kw = self.expect_ident_like()?;
7206            if !end_kw.eq_ignore_ascii_case("end") {
7207                return Err(self.err(alloc::format!(
7208                    "expected END LOOP after LOOP body, got {end_kw:?}"
7209                )));
7210            }
7211            let loop_kw = self.expect_ident_like()?;
7212            if !loop_kw.eq_ignore_ascii_case("loop") {
7213                return Err(self.err(alloc::format!(
7214                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7215                )));
7216            }
7217            return Ok(PlPgSqlStmt::Loop { body });
7218        }
7219        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7220        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7221        {
7222            self.advance();
7223            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7224            {
7225                self.advance();
7226                Some(self.parse_expr(0)?)
7227            } else {
7228                None
7229            };
7230            return Ok(PlPgSqlStmt::Exit { when });
7231        }
7232        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7233        // already-parsed Statement or a runtime-computed SQL string.
7234        // The disambiguator vs the extended-query-protocol `EXECUTE
7235        // <stmt_name>` (which is a top-level Statement, not a
7236        // plpgsql line) is that inside a DO block / trigger body the
7237        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7238        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7239        {
7240            self.advance();
7241            let sql = self.parse_expr(0)?;
7242            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7243        }
7244        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7245        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7246        {
7247            self.advance();
7248            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7249            {
7250                self.advance();
7251                Some(self.parse_expr(0)?)
7252            } else {
7253                None
7254            };
7255            return Ok(PlPgSqlStmt::Continue { when });
7256        }
7257        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7258        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7259        {
7260            self.advance();
7261            let condition = self.parse_expr(0)?;
7262            let loop_kw = self.expect_ident_like()?;
7263            if !loop_kw.eq_ignore_ascii_case("loop") {
7264                return Err(self.err(alloc::format!(
7265                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7266                )));
7267            }
7268            let body = self.parse_plpgsql_stmt_list_until_end()?;
7269            // Expect END LOOP.
7270            let end_kw = self.expect_ident_like()?;
7271            if !end_kw.eq_ignore_ascii_case("end") {
7272                return Err(self.err(alloc::format!(
7273                    "expected END LOOP after WHILE body, got {end_kw:?}"
7274                )));
7275            }
7276            let loop_kw2 = self.expect_ident_like()?;
7277            if !loop_kw2.eq_ignore_ascii_case("loop") {
7278                return Err(self.err(alloc::format!(
7279                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7280                )));
7281            }
7282            return Ok(PlPgSqlStmt::While { condition, body });
7283        }
7284        // v7.12.6 — RAISE.
7285        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7286        {
7287            self.advance();
7288            return self.parse_plpgsql_raise();
7289        }
7290        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7291        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7292        {
7293            self.advance();
7294            let condition = self.parse_expr(0)?;
7295            let message = if matches!(self.peek(), Token::Comma) {
7296                self.advance();
7297                Some(self.parse_expr(0)?)
7298            } else {
7299                None
7300            };
7301            return Ok(PlPgSqlStmt::Assert { condition, message });
7302        }
7303        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7304        //   "PERFORM is equivalent to SELECT but discards the
7305        //    result." Side effects (function calls, RAISE inside
7306        //    SQL functions, etc.) still execute. We desugar to
7307        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7308        //    existing embedded-statement path handles execution +
7309        //    result-discard cleanly. The result is naturally
7310        //    discarded because EmbeddedSql doesn't propagate row
7311        //    sets back to the plpgsql interpreter.
7312        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7313        {
7314            self.advance();
7315            // Splice a synthetic Token::Select into the stream at
7316            // the current position so parse_select_stmt parses the
7317            // remainder as a normal SELECT body. Token-stream
7318            // surgery mirrors the try_parse_plpgsql_select_into
7319            // pattern used for SELECT … INTO desugaring.
7320            self.tokens.insert(self.pos, Token::Select);
7321            let select = self.parse_select_stmt()?;
7322            let Statement::Select(s) = select else {
7323                return Err(self.err(alloc::format!(
7324                    "expected SELECT body after PERFORM, got {:?}",
7325                    self.peek()
7326                )));
7327            };
7328            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7329        }
7330        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7331        // plpgsql-specific shape (mailrs round-10 migrate-042).
7332        // PG's SELECT INTO at top-level SQL would CREATE a new
7333        // table; inside plpgsql it ASSIGNS the query result to
7334        // a local variable. We detect the INTO at paren-depth
7335        // 0 between SELECT and the statement boundary; if
7336        // found, split the token stream into "pre-INTO
7337        // projection" + "var" + "post-INTO FROM/WHERE…" and
7338        // rebuild as a SelectInto with a regular SELECT body
7339        // (no INTO clause).
7340        if matches!(self.peek(), Token::Select)
7341            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7342        {
7343            return Ok(PlPgSqlStmt::SelectInto {
7344                var: var_name,
7345                body: Box::new(select_body),
7346            });
7347        }
7348        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7349        // SELECT can appear directly inside a trigger body; we
7350        // recurse into the regular Statement parser, which will
7351        // stop at the trailing `;` (which our caller then
7352        // consumes).
7353        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7354        // also embed ALTER / CREATE / DROP statements; route
7355        // those through the same parser so the DO body parses
7356        // cleanly.
7357        if matches!(self.peek(), Token::Insert)
7358            || matches!(self.peek(), Token::Select)
7359            || matches!(self.peek(), Token::Create)
7360            || matches!(self.peek(), Token::Drop)
7361            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7362                if s.eq_ignore_ascii_case("update")
7363                    || s.eq_ignore_ascii_case("delete")
7364                    || s.eq_ignore_ascii_case("alter"))
7365        {
7366            let stmt = self.parse_one_statement()?;
7367            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7368        }
7369        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7370        // followed by `:=` and an expression.
7371        let target = self.parse_plpgsql_assign_target()?;
7372        // PL/pgSQL assignment uses `:=`. The lexer represents
7373        // this as a colon followed by `=`; check both shapes.
7374        match self.peek() {
7375            Token::ColonEq => {
7376                self.advance();
7377            }
7378            Token::Colon => {
7379                self.advance();
7380                if !matches!(self.peek(), Token::Eq) {
7381                    return Err(self.err(alloc::format!(
7382                        "expected := after plpgsql assign target, got `:` then {:?}",
7383                        self.peek()
7384                    )));
7385                }
7386                self.advance();
7387            }
7388            other => {
7389                return Err(self.err(alloc::format!(
7390                    "expected := after plpgsql assign target, got {other:?}"
7391                )));
7392            }
7393        }
7394        let value = self.parse_expr(0)?;
7395        Ok(PlPgSqlStmt::Assign { target, value })
7396    }
7397
7398    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7399    /// [ELSE body] END IF`. `IF` keyword already consumed.
7400    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7401        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7402        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7403        loop {
7404            // <expr> THEN
7405            let cond = self.parse_expr(0)?;
7406            let then_kw = self.expect_ident_like()?;
7407            if !then_kw.eq_ignore_ascii_case("then") {
7408                return Err(self.err(alloc::format!(
7409                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7410                )));
7411            }
7412            let body = self.parse_plpgsql_stmt_list_until_end()?;
7413            branches.push((cond, body));
7414            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7415            match self.peek() {
7416                Token::Ident(s) | Token::QuotedIdent(s)
7417                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7418                {
7419                    self.advance();
7420                    continue;
7421                }
7422                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7423                    self.advance();
7424                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7425                    break;
7426                }
7427                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7428                    break;
7429                }
7430                other => {
7431                    return Err(self.err(alloc::format!(
7432                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7433                    )));
7434                }
7435            }
7436        }
7437        // Expect `END IF` (the END keyword is the one we're
7438        // looking at right now).
7439        let end_kw = self.expect_ident_like()?;
7440        if !end_kw.eq_ignore_ascii_case("end") {
7441            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7442        }
7443        let if_kw = self.expect_ident_like()?;
7444        if !if_kw.eq_ignore_ascii_case("if") {
7445            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7446        }
7447        Ok(PlPgSqlStmt::If {
7448            branches,
7449            else_branch,
7450        })
7451    }
7452
7453    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7454    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7455    /// is already consumed.
7456    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7457        let lvl_ident = self.expect_ident_like()?;
7458        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7459            "notice" => RaiseLevel::Notice,
7460            "warning" => RaiseLevel::Warning,
7461            "info" => RaiseLevel::Info,
7462            "log" => RaiseLevel::Log,
7463            "debug" => RaiseLevel::Debug,
7464            "exception" => RaiseLevel::Exception,
7465            other => {
7466                return Err(self.err(alloc::format!(
7467                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7468                )));
7469            }
7470        };
7471        // Message: required for v7.12.6. PG accepts a bare
7472        // RAISE-rethrow form (no message), reserved for future
7473        // RAISE-no-args support.
7474        let Token::String(msg) = self.peek() else {
7475            return Err(self.err(alloc::format!(
7476                "expected RAISE message string, got {:?}",
7477                self.peek()
7478            )));
7479        };
7480        let message = msg.clone();
7481        self.advance();
7482        // Optional comma-separated args (PG `%` format substitution).
7483        let mut args: Vec<Expr> = Vec::new();
7484        while matches!(self.peek(), Token::Comma) {
7485            self.advance();
7486            args.push(self.parse_expr(0)?);
7487        }
7488        Ok(PlPgSqlStmt::Raise {
7489            level,
7490            message,
7491            args,
7492        })
7493    }
7494
7495    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7496    /// <projection> INTO <var> [FROM …]` (mailrs round-10
7497    /// migrate-042). Returns `(rebuilt_select_without_into,
7498    /// var_name)` when the pattern matches; `None` for
7499    /// regular SELECTs (those go through the embedded-SQL
7500    /// path). Token-stream surgery so the rebuilt SELECT
7501    /// parses through the regular `parse_select_stmt`.
7502    #[allow(clippy::too_many_lines)]
7503    fn try_parse_plpgsql_select_into(
7504        &mut self,
7505    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7506        // Scan forward from `self.pos + 1` (past Token::Select)
7507        // for Token::Into at paren-depth 0, stopping at the
7508        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7509        // end the plpgsql statement.
7510        let start = self.pos;
7511        let mut into_pos: Option<usize> = None;
7512        let mut depth: i32 = 0;
7513        let mut i = start + 1;
7514        while i < self.tokens.len() {
7515            match &self.tokens[i] {
7516                Token::LParen => depth += 1,
7517                Token::RParen => depth -= 1,
7518                Token::Semicolon if depth == 0 => break,
7519                Token::Ident(s)
7520                    if depth == 0
7521                        && (s.eq_ignore_ascii_case("end")
7522                            || s.eq_ignore_ascii_case("else")
7523                            || s.eq_ignore_ascii_case("elsif")) =>
7524                {
7525                    break;
7526                }
7527                Token::Into if depth == 0 => {
7528                    into_pos = Some(i);
7529                    break;
7530                }
7531                _ => {}
7532            }
7533            i += 1;
7534        }
7535        let Some(into_at) = into_pos else {
7536            return Ok(None);
7537        };
7538        // The token immediately after INTO must be the target
7539        // var ident; anything else (e.g. INSERT INTO table)
7540        // ruled out by the depth-0 check above. Capture it.
7541        let var = match self.tokens.get(into_at + 1) {
7542            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7543            other => {
7544                return Err(self.err(alloc::format!(
7545                    "expected variable name after SELECT … INTO, got {other:?}"
7546                )));
7547            }
7548        };
7549        // Find the end of the plpgsql SELECT INTO statement —
7550        // same boundary rules as the depth-0 scan above.
7551        let mut end = into_at + 2;
7552        let mut depth2: i32 = 0;
7553        while end < self.tokens.len() {
7554            match &self.tokens[end] {
7555                Token::LParen => depth2 += 1,
7556                Token::RParen => depth2 -= 1,
7557                Token::Semicolon if depth2 == 0 => break,
7558                Token::Ident(s)
7559                    if depth2 == 0
7560                        && (s.eq_ignore_ascii_case("end")
7561                            || s.eq_ignore_ascii_case("else")
7562                            || s.eq_ignore_ascii_case("elsif")) =>
7563                {
7564                    break;
7565                }
7566                _ => {}
7567            }
7568            end += 1;
7569        }
7570        // Rebuild a token stream that represents the SELECT
7571        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7572        // post-var tokens up to statement end]. Run the
7573        // regular `parse_select_stmt` against it.
7574        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7575        for j in start..into_at {
7576            rebuilt.push(self.tokens[j].clone());
7577        }
7578        for j in (into_at + 2)..end {
7579            rebuilt.push(self.tokens[j].clone());
7580        }
7581        rebuilt.push(Token::Eof);
7582        let saved_pos = self.pos;
7583        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7584        self.pos = 0;
7585        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7586        if !matches!(self.peek(), Token::Select) {
7587            self.tokens = saved_tokens;
7588            self.pos = saved_pos;
7589            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7590        }
7591        let sel = self.parse_select_stmt();
7592        self.tokens = saved_tokens;
7593        self.pos = end;
7594        let sel = sel?;
7595        let Statement::Select(body) = sel else {
7596            return Err(self.err(alloc::format!(
7597                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7598            )));
7599        };
7600        Ok(Some((body, var)))
7601    }
7602
7603    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7604        // v7.16.1 — read the head token DIRECTLY rather than
7605        // via `expect_ident_like`. The v7.14.0 schema-qualifier
7606        // strip (`public.t` → `t`) inside `expect_ident_like`
7607        // greedily consumes any `ident . ident` pair, which
7608        // silently turned every `NEW.col := …` /
7609        // `OLD.col := …` plpgsql assignment into a Local("col")
7610        // assignment — the head "new"/"old" was eaten as if it
7611        // were a schema name and the Dot was consumed too, so
7612        // this function's own `peek() == Token::Dot` check
7613        // below never fired. Every BEFORE trigger that rewrote
7614        // a NEW cell was a silent no-op for two major releases
7615        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7616        // gate failures were investigated as v7.16.1 backlog.
7617        let head = match self.advance() {
7618            Token::Ident(s) | Token::QuotedIdent(s) => s,
7619            other => {
7620                return Err(self.err(alloc::format!(
7621                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7622                )));
7623            }
7624        };
7625        if matches!(self.peek(), Token::Dot) {
7626            self.advance();
7627            let col = self.expect_ident_like()?;
7628            if head.eq_ignore_ascii_case("new") {
7629                return Ok(AssignTarget::NewColumn(col));
7630            }
7631            if head.eq_ignore_ascii_case("old") {
7632                return Ok(AssignTarget::OldColumn(col));
7633            }
7634            return Err(self.err(alloc::format!(
7635                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7636                 got {head:?}.<col>"
7637            )));
7638        }
7639        Ok(AssignTarget::Local(head))
7640    }
7641
7642    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7643        // RETURN NEW / OLD / NULL — bare-ident forms.
7644        match self.peek() {
7645            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7646                self.advance();
7647                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7648            }
7649            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7650                self.advance();
7651                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7652            }
7653            Token::Null => {
7654                self.advance();
7655                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7656            }
7657            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7658            // per PL/pgSQL convention.
7659            Token::Semicolon => {
7660                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7661            }
7662            _ => {}
7663        }
7664        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7665        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7666        // caller-visible effect (blocks don't return sets), so we
7667        // desugar it identically to PERFORM: parse the SELECT (or
7668        // EXECUTE dynamic) as embedded SQL that runs for side
7669        // effects and discards the result. RETURN NEXT <expr>
7670        // (single-row accumulator) queues with v7.40 SETOF function
7671        // infrastructure.
7672        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7673        // and keep going.
7674        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7675        {
7676            self.advance();
7677            let e = self.parse_expr(0)?;
7678            return Ok(PlPgSqlStmt::ReturnNext(e));
7679        }
7680        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7681        {
7682            self.advance();
7683            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7684            // rows go to the set, like the static form. It used to desugar to a
7685            // bare ExecuteDynamic, whose result was DISCARDED.
7686            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7687            {
7688                self.advance();
7689                let sql = self.parse_expr(0)?;
7690                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7691            }
7692            // Bare RETURN QUERY <select>. If the current token is
7693            // not already SELECT (e.g., the user wrote `RETURN QUERY
7694            // <projection> FROM ...` in a shorthand — rare but PG
7695            // accepts a bare projection here), splice one in. Same
7696            // trick as PERFORM.
7697            if !matches!(self.peek(), Token::Select) {
7698                self.tokens.insert(self.pos, Token::Select);
7699            }
7700            let select = self.parse_select_stmt()?;
7701            let Statement::Select(s) = select else {
7702                return Err(self.err(alloc::format!(
7703                    "expected SELECT body after RETURN QUERY, got {:?}",
7704                    self.peek()
7705                )));
7706            };
7707            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7708            // to an embedded side-effect SELECT whose rows were DISCARDED, which
7709            // in a SETOF function is the entire answer thrown away.
7710            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7711        }
7712        // Fall through: parse a full expression.
7713        let e = self.parse_expr(0)?;
7714        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7715    }
7716
7717    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7718        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7719        // are ident-shaped (the parser keys off case-insensitive
7720        // match — same shape used by the top-level Update / Delete
7721        // dispatchers at parse_one_statement).
7722        if matches!(self.peek(), Token::Insert) {
7723            self.advance();
7724            return Ok(TriggerEvent::Insert);
7725        }
7726        match self.peek() {
7727            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7728                self.advance();
7729                Ok(TriggerEvent::Update)
7730            }
7731            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7732                self.advance();
7733                Ok(TriggerEvent::Delete)
7734            }
7735            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7736                self.advance();
7737                Ok(TriggerEvent::Truncate)
7738            }
7739            other => Err(self.err(alloc::format!(
7740                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
7741            ))),
7742        }
7743    }
7744
7745    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
7746    ///   - (no clause) → implicit `FOR ALL TABLES`
7747    ///   - `FOR ALL TABLES`
7748    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
7749    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
7750    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
7751    ///     REJECTS the bare plural (`invalid publication object list`,
7752    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
7753    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
7754    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
7755        let name = self.expect_ident_or_string()?;
7756        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
7757        // shape so existing publications keep parsing identically.
7758        let scope = if matches!(self.peek(), Token::For) {
7759            self.advance();
7760            if matches!(self.peek(), Token::All) {
7761                self.advance();
7762                if !matches!(self.peek(), Token::Tables) {
7763                    return Err(self.err(format!(
7764                        "expected TABLES after FOR ALL, got {:?}",
7765                        self.peek()
7766                    )));
7767                }
7768                self.advance();
7769                if matches!(self.peek(), Token::Except) {
7770                    self.advance();
7771                    let tables = self.parse_publication_table_list()?;
7772                    PublicationScope::AllTablesExcept(tables)
7773                } else {
7774                    PublicationScope::AllTables
7775                }
7776            } else if matches!(self.peek(), Token::Table) {
7777                self.advance();
7778                let tables = self.parse_publication_table_list()?;
7779                PublicationScope::ForTables(tables)
7780            } else if matches!(self.peek(), Token::Tables) {
7781                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
7782                // plural (`FOR TABLES t`) is REJECTED (`invalid
7783                // publication object list`); TABLES only pairs with
7784                // `IN SCHEMA`. The old arm accepted it on an
7785                // unverifiable "PG 19 accepts both" claim.
7786                self.advance();
7787                if !matches!(self.peek(), Token::In) {
7788                    return Err(self.err(alloc::string::String::from(
7789                        "invalid publication object list",
7790                    )));
7791                }
7792                self.advance();
7793                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
7794                    return Err(self.err(format!(
7795                        "expected SCHEMA after FOR TABLES IN, got {:?}",
7796                        self.peek()
7797                    )));
7798                }
7799                self.advance();
7800                let schema = self.expect_ident_or_string()?;
7801                PublicationScope::TablesInSchema(schema)
7802            } else {
7803                return Err(self.err(format!(
7804                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
7805                    self.peek()
7806                )));
7807            }
7808        } else {
7809            PublicationScope::AllTables
7810        };
7811        Ok(Statement::CreatePublication(CreatePublicationStatement {
7812            name,
7813            scope,
7814        }))
7815    }
7816
7817    /// v6.1.3 — Comma-separated identifier list for the publication
7818    /// FOR-clause. Requires at least one entry; empty list is a
7819    /// parse error (PG behaviour). Quoted idents are accepted; the
7820    /// names round-trip through `Display` as `quote_ident(name)`.
7821    ///
7822    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
7823    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
7824    /// pg_dump output. SPG's publication state today is per-table
7825    /// only (matching the pre-PG-15 surface); the col list + WHERE
7826    /// are parsed so dumps load through and the table name reaches
7827    /// `PublicationScope::ForTables`, but the filter is not enforced
7828    /// at publish time. Re-open when a customer dogfood gate
7829    /// requires per-row-filter or column-subset publish semantics
7830    /// (which gates on persistent slot state landing first, 21.12).
7831    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
7832        let first = self.parse_publication_table_entry()?;
7833        let mut out = alloc::vec![first];
7834        while matches!(self.peek(), Token::Comma) {
7835            self.advance();
7836            out.push(self.parse_publication_table_entry()?);
7837        }
7838        Ok(out)
7839    }
7840
7841    /// One table entry inside a FOR TABLE clause:
7842    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
7843    /// Returns just the table name; the column list + WHERE predicate
7844    /// are consumed and discarded per the parse-accept-discard
7845    /// commitment above.
7846    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
7847        let name = self.expect_ident_like()?;
7848        // Optional column list — `(col, col, …)`.
7849        if matches!(self.peek(), Token::LParen) {
7850            self.advance();
7851            // Empty parens are a PG error too; require ≥ 1 column.
7852            let _ = self.expect_ident_like()?;
7853            while matches!(self.peek(), Token::Comma) {
7854                self.advance();
7855                let _ = self.expect_ident_like()?;
7856            }
7857            if !matches!(self.peek(), Token::RParen) {
7858                return Err(self.err(alloc::format!(
7859                    "expected ')' to close publication column list, got {:?}",
7860                    self.peek()
7861                )));
7862            }
7863            self.advance();
7864        }
7865        // Optional row filter — `WHERE (predicate)`.
7866        if matches!(self.peek(), Token::Where) {
7867            self.advance();
7868            if !matches!(self.peek(), Token::LParen) {
7869                return Err(self.err(alloc::format!(
7870                    "expected '(' after WHERE in publication row filter, got {:?}",
7871                    self.peek()
7872                )));
7873            }
7874            self.advance();
7875            let _ = self.parse_expr(0)?;
7876            if !matches!(self.peek(), Token::RParen) {
7877                return Err(self.err(alloc::format!(
7878                    "expected ')' to close publication WHERE filter, got {:?}",
7879                    self.peek()
7880                )));
7881            }
7882            self.advance();
7883        }
7884        Ok(name)
7885    }
7886
7887    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
7888    ///                 CONNECTION '<conn>'
7889    ///                 PUBLICATION <pub> [, <pub> ...]`.
7890    ///
7891    /// The clause order is fixed (CONNECTION first, then
7892    /// PUBLICATION) to match PG. No WITH-options accepted in
7893    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
7894    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
7895        let name = self.expect_ident_or_string()?;
7896        if !matches!(self.peek(), Token::Connection) {
7897            return Err(self.err(format!(
7898                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
7899                self.peek()
7900            )));
7901        }
7902        self.advance();
7903        let conn_str = self.expect_string_literal()?;
7904        if !matches!(self.peek(), Token::Publication) {
7905            return Err(self.err(format!(
7906                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
7907                self.peek()
7908            )));
7909        }
7910        self.advance();
7911        // Reuse the publication FOR-list parser shape: at least one
7912        // identifier, comma-separated.
7913        let first = self.expect_ident_like()?;
7914        let mut publications = alloc::vec![first];
7915        while matches!(self.peek(), Token::Comma) {
7916            self.advance();
7917            publications.push(self.expect_ident_like()?);
7918        }
7919        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
7920            name,
7921            conn_str,
7922            publications,
7923        }))
7924    }
7925
7926    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
7927    /// All keywords after `WAIT` are bare idents in v6.1.x; no
7928    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
7929    /// that fit `u64`.
7930    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
7931    /// qualifier is a *namespace* the app owns (`app.user_id`,
7932    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
7933    /// to discard. So parse the raw segments here instead of
7934    /// `expect_ident_like`, which strips a leading `schema.` qualifier
7935    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
7936    /// a single segment and round-trip unchanged.
7937    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
7938        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
7939        loop {
7940            let seg = match self.advance() {
7941                Token::Ident(s) | Token::QuotedIdent(s) => s,
7942                other if unreserved_keyword_text(&other).is_some() => {
7943                    unreserved_keyword_text(&other).unwrap()
7944                }
7945                other => {
7946                    return Err(ParseError {
7947                        message: format!("expected parameter name, got {other:?}"),
7948                        token_pos: self.consumed_pos(),
7949                    });
7950                }
7951            };
7952            parts.push(seg);
7953            if matches!(self.peek(), Token::Dot) {
7954                self.advance();
7955                continue;
7956            }
7957            break;
7958        }
7959        Ok(parts.join(".").to_ascii_lowercase())
7960    }
7961
7962    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
7963        Self::parse_set_value_inner(self)
7964    }
7965
7966    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
7967        match self.advance() {
7968            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
7969            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
7970                Ok(crate::ast::SetValue::Default)
7971            }
7972            Token::Ident(s) | Token::QuotedIdent(s) => {
7973                let mut accum = s;
7974                while matches!(self.peek(), Token::Dot) {
7975                    self.advance();
7976                    let next = self.expect_ident_like()?;
7977                    accum.push('.');
7978                    accum.push_str(&next);
7979                }
7980                Ok(crate::ast::SetValue::Ident(accum))
7981            }
7982            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
7983            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
7984            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
7985            // spellings that lex as keyword tokens, not idents:
7986            // `SET standard_conforming_strings = on` is in every
7987            // pg_dump preamble (`off` already lexes as an ident).
7988            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
7989            // DEFAULT lexes as its keyword token, so the ident arm above
7990            // never saw it and the everyday reset form was a syntax error.
7991            Token::Default => Ok(crate::ast::SetValue::Default),
7992            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
7993            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
7994            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
7995            // v7.14.0 — MySQL session/user variable RHS
7996            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
7997            // Wrap as Ident so the SET handler can record it; the
7998            // engine treats `@VAR` / `@@VAR` values as opaque
7999            // strings.
8000            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8001            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8002            // is the common MySQL preamble shape. Allow a `+` or
8003            // `-` prefix on negative numerics for parity with PG
8004            // (some param defaults are negative).
8005            Token::Minus => match self.advance() {
8006                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8007                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8008                other => Err(self.err(format!(
8009                    "expected numeric after `-` in SET value, got {other:?}"
8010                ))),
8011            },
8012            other => Err(self.err(format!(
8013                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8014            ))),
8015        }
8016    }
8017
8018    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8019    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8020    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8021    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8022    /// present). Modes are comma-separated per PG; SPG also
8023    /// accepts space-separated for tolerance. READ ONLY / WRITE
8024    /// / DEFERRABLE are parsed-and-ignored (recorded for future
8025    /// surface but not behaviorally honoured today).
8026    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8027    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8028    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8029    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8030    /// session default rather than forcing READ COMMITTED.
8031    fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8032        let mut level = IsolationLevel::default();
8033        let mut have_level = false;
8034        loop {
8035            // ISOLATION LEVEL …
8036            let saw_isolation =
8037                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8038            if saw_isolation {
8039                self.advance(); // ISOLATION
8040                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8041                    return Err(self.err(alloc::format!(
8042                        "expected LEVEL after ISOLATION, got {:?}",
8043                        self.peek()
8044                    )));
8045                }
8046                self.advance(); // LEVEL
8047                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8048                let w1 = self
8049                    .expect_ident_like()
8050                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8051                let lc = w1.to_ascii_lowercase();
8052                level = match lc.as_str() {
8053                    "serializable" => IsolationLevel::Serializable,
8054                    "repeatable" => {
8055                        // Expect READ
8056                        let w2 = self
8057                            .expect_ident_like()
8058                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8059                        if !w2.eq_ignore_ascii_case("read") {
8060                            return Err(self.err(alloc::format!(
8061                                "expected READ after REPEATABLE, got {w2:?}"
8062                            )));
8063                        }
8064                        IsolationLevel::RepeatableRead
8065                    }
8066                    "read" => {
8067                        let w2 = self
8068                            .expect_ident_like()
8069                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8070                        match w2.to_ascii_lowercase().as_str() {
8071                            "committed" => IsolationLevel::ReadCommitted,
8072                            "uncommitted" => IsolationLevel::ReadUncommitted,
8073                            other => {
8074                                return Err(self.err(alloc::format!(
8075                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8076                                )));
8077                            }
8078                        }
8079                    }
8080                    other => {
8081                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8082                    }
8083                };
8084                have_level = true;
8085            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8086                // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8087                self.advance();
8088                match self.peek().clone() {
8089                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8090                        self.advance();
8091                    }
8092                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8093                        self.advance();
8094                    }
8095                    other => {
8096                        return Err(self.err(alloc::format!(
8097                            "expected ONLY or WRITE after READ, got {other:?}"
8098                        )));
8099                    }
8100                }
8101            } else if matches!(self.peek(), Token::Not) {
8102                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8103                self.advance();
8104                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8105                    return Err(self.err(alloc::format!(
8106                        "expected DEFERRABLE after NOT, got {:?}",
8107                        self.peek()
8108                    )));
8109                }
8110                self.advance();
8111            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8112            {
8113                self.advance();
8114            } else {
8115                break;
8116            }
8117            // Optional comma between modes.
8118            if matches!(self.peek(), Token::Comma) {
8119                self.advance();
8120            }
8121        }
8122        Ok(have_level.then_some(level))
8123    }
8124
8125    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8126        // FOR is a v6.1.2-reserved keyword (Token::For). The
8127        // other two are bare idents — they've never needed lexer
8128        // support and we keep it that way.
8129        if !matches!(self.peek(), Token::For) {
8130            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8131        }
8132        self.advance();
8133        self.expect_keyword_ident("wal")?;
8134        self.expect_keyword_ident("position")?;
8135        let pos = self.expect_u64_literal()?;
8136        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8137        {
8138            self.advance();
8139            self.expect_keyword_ident("timeout")?;
8140            Some(self.expect_u64_literal()?)
8141        } else {
8142            None
8143        };
8144        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8145    }
8146
8147    /// v6.1.7 helper — consume a `Token::Integer` and check it
8148    /// fits `u64`. WAL positions and millisecond timeouts are
8149    /// non-negative.
8150    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8151        match self.advance() {
8152            Token::Integer(n) if n >= 0 => Ok(n as u64),
8153            Token::Integer(n) => Err(ParseError {
8154                message: format!("expected non-negative integer, got {n}"),
8155                token_pos: self.consumed_pos(),
8156            }),
8157            other => Err(ParseError {
8158                message: format!("expected integer literal, got {other:?}"),
8159                token_pos: self.consumed_pos(),
8160            }),
8161        }
8162    }
8163
8164    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8165    /// ROLE '<role>' (defaults to readonly). All string slots accept
8166    /// either a quoted ident or a quoted string literal.
8167    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8168    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8169    ///
8170    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8171    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8172    /// wire role) still parses — it is a different axis from the PG attributes.
8173    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8174    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8175    /// or RESET, so the plain attribute forms keep their old path.
8176    fn peeks_db_role_setting(&self) -> bool {
8177        let mut i = self.pos + 1; // past the object's name
8178        let word = |p: usize| -> Option<String> {
8179            match self.tokens.get(p) {
8180                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8181                Some(Token::In) => Some(String::from("in")),
8182                _ => None,
8183            }
8184        };
8185        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8186            i += 3; // IN DATABASE <name>
8187        }
8188        matches!(word(i).as_deref(), Some("set" | "reset"))
8189    }
8190
8191    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8192        use crate::ast::SetDbRoleSettingStatement;
8193        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8194        // identifier, so the ordinary name reader refuses it. Same trap
8195        // as TABLE / INDEX / FULL / DEFAULT before it.
8196        let name = if matches!(self.peek(), Token::All) {
8197            self.advance();
8198            String::from("all")
8199        } else {
8200            self.expect_ident_or_string()?
8201        };
8202        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8203        let all = name.eq_ignore_ascii_case("all");
8204        let (mut database, mut role) = if is_database {
8205            (Some(name), None)
8206        } else if all {
8207            (None, None)
8208        } else {
8209            (None, Some(name))
8210        };
8211        if matches!(self.peek(), Token::In) {
8212            self.advance();
8213            self.advance(); // DATABASE
8214            database = Some(self.expect_ident_or_string()?);
8215        }
8216        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8217        self.advance(); // SET | RESET
8218        if resetting && matches!(self.peek(), Token::All) {
8219            self.advance();
8220            self.consume_until_statement_boundary();
8221            return Ok(Statement::SetDbRoleSetting(Box::new(
8222                SetDbRoleSettingStatement {
8223                    database,
8224                    role,
8225                    param: None,
8226                    value: None,
8227                },
8228            )));
8229        }
8230        let param = self.expect_ident_like()?;
8231        let value = if resetting {
8232            None
8233        } else {
8234            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8235            // KEYWORD, so the ident-only check missed it and consumed
8236            // the word itself as the value — the same trap as ALL, one
8237            // clause over.
8238            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8239                self.advance();
8240            }
8241            Some(self.take_guc_value())
8242        };
8243        self.consume_until_statement_boundary();
8244        Ok(Statement::SetDbRoleSetting(Box::new(
8245            SetDbRoleSettingStatement {
8246                database,
8247                role,
8248                param: Some(param),
8249                value,
8250            },
8251        )))
8252    }
8253
8254    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8255    /// a quoted literal loses its quotes, a bare word or number does not.
8256    fn take_guc_value(&mut self) -> String {
8257        match self.advance() {
8258            Token::String(s) => s,
8259            Token::Integer(n) => format!("{n}"),
8260            Token::Float(f) => format!("{f}"),
8261            Token::Ident(s) | Token::QuotedIdent(s) => s,
8262            other => format!("{other:?}"),
8263        }
8264    }
8265
8266    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8267        let name = self.expect_ident_or_string()?;
8268        if self.peek_keyword_ident("with") {
8269            self.advance();
8270        }
8271        let mut password = String::new();
8272        let mut role = String::new();
8273        let mut login: Option<bool> = None;
8274        let mut inherit: Option<bool> = None;
8275        let mut superuser: Option<bool> = None;
8276        // Not a `while let`: the pattern would borrow `self` across the
8277        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8278        #[allow(clippy::while_let_loop)]
8279        loop {
8280            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8281                break;
8282            };
8283            match w.to_ascii_lowercase().as_str() {
8284                "password" => {
8285                    self.advance();
8286                    password = self.expect_string_literal()?;
8287                }
8288                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8289                // is the same slot.
8290                "encrypted" => {
8291                    self.advance();
8292                    self.expect_keyword_ident("password")?;
8293                    password = self.expect_string_literal()?;
8294                }
8295                "login" => {
8296                    self.advance();
8297                    login = Some(true);
8298                }
8299                "nologin" => {
8300                    self.advance();
8301                    login = Some(false);
8302                }
8303                "inherit" => {
8304                    self.advance();
8305                    inherit = Some(true);
8306                }
8307                "noinherit" => {
8308                    self.advance();
8309                    inherit = Some(false);
8310                }
8311                "superuser" => {
8312                    self.advance();
8313                    superuser = Some(true);
8314                }
8315                "nosuperuser" => {
8316                    self.advance();
8317                    superuser = Some(false);
8318                }
8319                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8320                "role" => {
8321                    self.advance();
8322                    role = self.expect_string_literal()?;
8323                }
8324                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8325                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8326                // accepted and ignored so a pg_dump role block restores. They
8327                // gate capabilities SPG does not have.
8328                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8329                | "noreplication" | "bypassrls" | "nobypassrls" => {
8330                    self.advance();
8331                }
8332                "connection" => {
8333                    self.advance();
8334                    self.expect_keyword_ident("limit")?;
8335                    self.advance(); // the number
8336                }
8337                "valid" => {
8338                    self.advance();
8339                    self.expect_keyword_ident("until")?;
8340                    self.expect_string_literal()?;
8341                }
8342                _ => break,
8343            }
8344        }
8345        if role.is_empty() {
8346            role = "readonly".to_string();
8347        }
8348        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8349            name,
8350            password,
8351            role,
8352            login,
8353            inherit,
8354            superuser,
8355            is_user,
8356        }))
8357    }
8358
8359    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8360    /// consumed the USING / WITH CHECK keyword.
8361    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8362        if !matches!(self.peek(), Token::LParen) {
8363            return Err(self.err(alloc::format!(
8364                "expected '(' after {clause}, got {:?}",
8365                self.peek()
8366            )));
8367        }
8368        self.advance();
8369        let e = self.parse_expr(0)?;
8370        if !matches!(self.peek(), Token::RParen) {
8371            return Err(self.err(alloc::format!(
8372                "expected ')' to close {clause}, got {:?}",
8373                self.peek()
8374            )));
8375        }
8376        self.advance();
8377        Ok(e)
8378    }
8379
8380    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8381    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8382        let mut roles = Vec::new();
8383        loop {
8384            roles.push(self.expect_ident_like()?);
8385            if matches!(self.peek(), Token::Comma) {
8386                self.advance();
8387            } else {
8388                break;
8389            }
8390        }
8391        Ok(roles)
8392    }
8393
8394    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8395    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8396    /// `CREATE POLICY`.
8397    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8398        use crate::ast::PolicyCmd;
8399        let name = self.expect_ident_like()?;
8400        if !matches!(self.peek(), Token::On) {
8401            return Err(self.err(alloc::format!(
8402                "expected ON after CREATE POLICY name, got {:?}",
8403                self.peek()
8404            )));
8405        }
8406        self.advance();
8407        let table = self.expect_ident_like()?;
8408
8409        let mut permissive = true;
8410        if matches!(self.peek(), Token::As) {
8411            self.advance();
8412            let w = self.expect_ident_like()?;
8413            permissive = if w.eq_ignore_ascii_case("permissive") {
8414                true
8415            } else if w.eq_ignore_ascii_case("restrictive") {
8416                false
8417            } else {
8418                return Err(self.err(alloc::format!(
8419                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8420                )));
8421            };
8422        }
8423
8424        let mut cmd = PolicyCmd::All;
8425        if matches!(self.peek(), Token::For) {
8426            self.advance();
8427            cmd = self.parse_policy_cmd()?;
8428        }
8429
8430        let mut roles = Vec::new();
8431        if matches!(self.peek(), Token::To) {
8432            self.advance();
8433            roles = self.parse_policy_roles()?;
8434        }
8435
8436        let mut using = None;
8437        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8438        {
8439            self.advance();
8440            using = Some(self.parse_paren_expr("USING")?);
8441        }
8442
8443        let mut with_check = None;
8444        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8445        {
8446            self.advance();
8447            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8448            {
8449                return Err(self.err(alloc::format!(
8450                    "expected CHECK after WITH, got {:?}",
8451                    self.peek()
8452                )));
8453            }
8454            self.advance();
8455            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8456        }
8457
8458        // Clause-per-command matrix (PG wording).
8459        match cmd {
8460            PolicyCmd::Insert => {
8461                if using.is_some() {
8462                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8463                }
8464            }
8465            PolicyCmd::Select | PolicyCmd::Delete => {
8466                if with_check.is_some() {
8467                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8468                }
8469            }
8470            PolicyCmd::Update | PolicyCmd::All => {}
8471        }
8472
8473        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8474            name,
8475            table,
8476            permissive,
8477            cmd,
8478            roles,
8479            using,
8480            with_check,
8481        }))
8482    }
8483
8484    /// v7.39 (RLS) — the command word after `FOR`.
8485    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8486        use crate::ast::PolicyCmd;
8487        match self.peek().clone() {
8488            Token::All => {
8489                self.advance();
8490                Ok(PolicyCmd::All)
8491            }
8492            Token::Select => {
8493                self.advance();
8494                Ok(PolicyCmd::Select)
8495            }
8496            Token::Insert => {
8497                self.advance();
8498                Ok(PolicyCmd::Insert)
8499            }
8500            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8501                self.advance();
8502                Ok(PolicyCmd::Update)
8503            }
8504            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8505                self.advance();
8506                Ok(PolicyCmd::Delete)
8507            }
8508            other => Err(self.err(alloc::format!(
8509                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8510            ))),
8511        }
8512    }
8513
8514    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8515    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8516    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8517        let name = self.expect_ident_like()?;
8518        if !matches!(self.peek(), Token::On) {
8519            return Err(self.err(alloc::format!(
8520                "expected ON after ALTER POLICY name, got {:?}",
8521                self.peek()
8522            )));
8523        }
8524        self.advance();
8525        let table = self.expect_ident_like()?;
8526
8527        // RENAME TO new
8528        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8529        {
8530            self.advance();
8531            if !matches!(self.peek(), Token::To) {
8532                return Err(self.err(alloc::format!(
8533                    "expected TO after RENAME, got {:?}",
8534                    self.peek()
8535                )));
8536            }
8537            self.advance();
8538            let new = self.expect_ident_like()?;
8539            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8540                name,
8541                table,
8542                rename_to: Some(new),
8543                roles: None,
8544                using: None,
8545                with_check: None,
8546            }));
8547        }
8548
8549        let mut roles = None;
8550        if matches!(self.peek(), Token::To) {
8551            self.advance();
8552            roles = Some(self.parse_policy_roles()?);
8553        }
8554        let mut using = None;
8555        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8556        {
8557            self.advance();
8558            using = Some(self.parse_paren_expr("USING")?);
8559        }
8560        let mut with_check = None;
8561        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8562        {
8563            self.advance();
8564            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8565            {
8566                return Err(self.err(alloc::format!(
8567                    "expected CHECK after WITH, got {:?}",
8568                    self.peek()
8569                )));
8570            }
8571            self.advance();
8572            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8573        }
8574        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8575            name,
8576            table,
8577            rename_to: None,
8578            roles,
8579            using,
8580            with_check,
8581        }))
8582    }
8583
8584    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8585    /// `DROP POLICY`.
8586    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8587        let if_exists = self.consume_if_exists();
8588        let name = self.expect_ident_like()?;
8589        if !matches!(self.peek(), Token::On) {
8590            return Err(self.err(alloc::format!(
8591                "expected ON after DROP POLICY name, got {:?}",
8592                self.peek()
8593            )));
8594        }
8595        self.advance();
8596        let table = self.expect_ident_like()?;
8597        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8598            name,
8599            table,
8600            if_exists,
8601        }))
8602    }
8603}
8604fn wrap_from_leaves(
8605    e: &mut Expr,
8606    names: &[String],
8607    make: &dyn Fn(Expr) -> Expr,
8608    refs: &dyn Fn(&Expr) -> bool,
8609) {
8610    if let Expr::Column(c) = e {
8611        if c.qualifier
8612            .as_deref()
8613            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8614        {
8615            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8616            *e = make(taken);
8617        }
8618        return;
8619    }
8620    match e {
8621        Expr::Binary { lhs, rhs, .. } => {
8622            wrap_from_leaves(lhs, names, make, refs);
8623            wrap_from_leaves(rhs, names, make, refs);
8624        }
8625        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8626            wrap_from_leaves(expr, names, make, refs)
8627        }
8628        Expr::FunctionCall { args, .. } => {
8629            for a in args.iter_mut() {
8630                wrap_from_leaves(a, names, make, refs);
8631            }
8632        }
8633        Expr::Case {
8634            operand,
8635            branches,
8636            else_branch,
8637        } => {
8638            if let Some(o) = operand.as_deref_mut() {
8639                wrap_from_leaves(o, names, make, refs);
8640            }
8641            for (w, t) in branches.iter_mut() {
8642                wrap_from_leaves(w, names, make, refs);
8643                wrap_from_leaves(t, names, make, refs);
8644            }
8645            if let Some(el) = else_branch.as_deref_mut() {
8646                wrap_from_leaves(el, names, make, refs);
8647            }
8648        }
8649        // Compound variants the walk doesn't decompose: keep the
8650        // pre-D.30 behavior — wrap the whole sub-expr if it touches
8651        // a source table, so nothing regresses.
8652        other => {
8653            if refs(other) {
8654                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8655                *other = make(taken);
8656            }
8657        }
8658    }
8659}
8660
8661/// v7.39 (round 241) — does this expression reference any of the FROM /
8662/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8663/// lowerings)?
8664fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8665    match e {
8666        Expr::Column(c) => c
8667            .qualifier
8668            .as_deref()
8669            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8670        Expr::Binary { lhs, rhs, .. } => {
8671            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8672        }
8673        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8674        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8675        Expr::Case {
8676            operand,
8677            branches,
8678            else_branch,
8679        } => {
8680            operand
8681                .as_deref()
8682                .is_some_and(|o| expr_refs_tables(o, names))
8683                || branches
8684                    .iter()
8685                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8686                || else_branch
8687                    .as_deref()
8688                    .is_some_and(|el| expr_refs_tables(el, names))
8689        }
8690        _ => false,
8691    }
8692}
8693
8694impl Parser {
8695    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8696    /// Caller already consumed the leading `UPDATE` ident.
8697    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8698    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8699    /// after the target name has been read. `JOIN` is a reserved token;
8700    /// the qualifiers are bare idents.
8701    fn peek_is_update_join_start(&self) -> bool {
8702        match self.peek() {
8703            // JOIN and its qualifiers are reserved lexer tokens (the grammar
8704            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8705            Token::Join
8706            | Token::Inner
8707            | Token::Left
8708            | Token::Right
8709            | Token::Cross
8710            | Token::Full => true,
8711            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8712            Token::Ident(s) | Token::QuotedIdent(s) => {
8713                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8714            }
8715            _ => false,
8716        }
8717    }
8718
8719    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8720    /// USER-variable assignment. Its own per-session namespace, an arbitrary
8721    /// expression on the right, and `:=` as a second spelling of `=`.
8722    ///
8723    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8724    /// from sits on the nesting recursion chain (a CTE body, a subquery),
8725    /// and holding this loop's `Vec` + `String` locals there overflowed the
8726    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8727    #[inline(never)]
8728    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8729        let mut assigns: Vec<(String, Expr)> = Vec::new();
8730        let mut settings: Vec<(String, Expr)> = Vec::new();
8731        loop {
8732            // v7.39 (round 554) — a plain NAME here is a session
8733            // setting, not a user variable. mysqldump writes the two in
8734            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8735            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8736            // changes it — and this refused the mixture outright, so no
8737            // dump could be restored past its preamble.
8738            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
8739                self.advance();
8740                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8741                    return Err(self.err(alloc::format!(
8742                        "expected `=` after {name}, got {:?}",
8743                        self.peek()
8744                    )));
8745                }
8746                self.advance();
8747                let value = self.parse_expr(0)?;
8748                settings.push((name.to_ascii_lowercase(), value));
8749                if matches!(self.peek(), Token::Comma) {
8750                    self.advance();
8751                    continue;
8752                }
8753                break;
8754            }
8755            let Token::SessionVar(raw) = self.peek().clone() else {
8756                return Err(self.err(alloc::format!(
8757                    "expected a user variable after SET, got {:?}",
8758                    self.peek()
8759                )));
8760            };
8761            if raw.starts_with("@@") {
8762                return Err(self.err(alloc::string::String::from(
8763                    "cannot mix `@@` settings with `@` user variables in one SET",
8764                )));
8765            }
8766            self.advance();
8767            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8768                return Err(self.err(alloc::format!(
8769                    "expected `=` or `:=` after {raw}, got {:?}",
8770                    self.peek()
8771                )));
8772            }
8773            self.advance();
8774            let value = self.parse_expr(0)?;
8775            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
8776            if matches!(self.peek(), Token::Comma) {
8777                self.advance();
8778                continue;
8779            }
8780            break;
8781        }
8782        Ok(Statement::SetUserVars(assigns, settings))
8783    }
8784
8785    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
8786        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
8787        // NAMED `only` until now, which failed on `relation "only" does
8788        // not exist`. The lookahead is what keeps a table actually
8789        // called `only` working: the keyword is only a keyword when a
8790        // TABLE NAME follows it — and `SET` arrives as an identifier
8791        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
8792        // for the table and die on the `=`. Measured by the pin.
8793        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
8794            if s.eq_ignore_ascii_case("only"))
8795            && matches!(
8796                self.tokens.get(self.pos + 1),
8797                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
8798            );
8799        if only {
8800            self.advance();
8801        }
8802        let table = self.expect_ident_like()?;
8803        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
8804        // bare spelling; a bare identifier that is the SET keyword itself
8805        // is the clause, not an alias.
8806        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
8807        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
8808        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
8809        // following JOIN a syntax error.
8810        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
8811        let alias = if matches!(self.peek(), Token::As) {
8812            self.advance();
8813            Some(self.expect_ident_like()?)
8814        } else {
8815            match self.peek() {
8816                Token::Ident(s) | Token::QuotedIdent(s)
8817                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
8818                {
8819                    let a = s.clone();
8820                    self.advance();
8821                    Some(a)
8822                }
8823                _ => None,
8824            }
8825        };
8826        // v7.39 (round 420) — MySQL's multi-table UPDATE:
8827        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
8828        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
8829        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
8830        // The FIRST table is the mutation target and the rest are sources —
8831        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
8832        // SPG already lowers onto correlated subqueries. So rewind, let
8833        // `parse_from_clause` read the whole list (it handles aliases, comma
8834        // lists, and every JOIN form), then peel the target off the front.
8835        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
8836            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
8837        {
8838            // NOTE: `advance()` destroys the tokens it returns
8839            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
8840            // is NOT possible — the tail is read forward, once, through the
8841            // same grammar `parse_from_clause` uses after its primary.
8842            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
8843            let mut joins = self.parse_from_joins(&target_qual)?;
8844            if joins.is_empty() {
8845                return Err(self.err(alloc::string::String::from(
8846                    "multi-table UPDATE needs at least one source table",
8847                )));
8848            }
8849            let head = joins.remove(0);
8850            // A LEFT join keeps every target row (the unmatched ones see NULL
8851            // on the source side), so it must NOT get the EXISTS row filter
8852            // the inner / comma forms use.
8853            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
8854            let src = FromClause {
8855                primary: head.table,
8856                joins,
8857            };
8858            (Some(src), head.on, outer)
8859        } else {
8860            (None, None, false)
8861        };
8862        self.expect_keyword_ident("set")?;
8863        let mut assignments = Vec::new();
8864        loop {
8865            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
8866            // …)` — the parenthesized multi-assignment. Expressions
8867            // assign positionally; a subquery RHS clones per column
8868            // keeping only the Nth projection item.
8869            if matches!(self.peek(), Token::LParen) {
8870                self.advance();
8871                let mut cols = alloc::vec![self.expect_ident_like()?];
8872                while matches!(self.peek(), Token::Comma) {
8873                    self.advance();
8874                    cols.push(self.expect_ident_like()?);
8875                }
8876                if !matches!(self.peek(), Token::RParen) {
8877                    return Err(self.err(format!(
8878                        "expected ')' after SET column list, got {:?}",
8879                        self.peek()
8880                    )));
8881                }
8882                self.advance();
8883                if !matches!(self.peek(), Token::Eq) {
8884                    return Err(self.err(format!(
8885                        "expected `=` after SET column list, got {:?}",
8886                        self.peek()
8887                    )));
8888                }
8889                self.advance();
8890                if !matches!(self.peek(), Token::LParen) {
8891                    return Err(self.err(format!(
8892                        "expected '(' after SET (…) =, got {:?}",
8893                        self.peek()
8894                    )));
8895                }
8896                self.advance();
8897                if matches!(self.peek(), Token::Select) {
8898                    let inner = match self.parse_select_stmt()? {
8899                        Statement::Select(s) => s,
8900                        other => {
8901                            return Err(self.err(alloc::format!(
8902                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
8903                            )));
8904                        }
8905                    };
8906                    if !matches!(self.peek(), Token::RParen) {
8907                        return Err(self.err(format!(
8908                            "expected ')' after SET subquery, got {:?}",
8909                            self.peek()
8910                        )));
8911                    }
8912                    self.advance();
8913                    if inner.items.len() != cols.len() {
8914                        return Err(self.err(alloc::format!(
8915                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
8916                            cols.len(),
8917                            inner.items.len()
8918                        )));
8919                    }
8920                    for (i, col) in cols.into_iter().enumerate() {
8921                        let mut sub = inner.clone();
8922                        sub.items = alloc::vec![sub.items[i].clone()];
8923                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
8924                    }
8925                } else {
8926                    let mut exprs = alloc::vec![self.parse_expr(0)?];
8927                    while matches!(self.peek(), Token::Comma) {
8928                        self.advance();
8929                        exprs.push(self.parse_expr(0)?);
8930                    }
8931                    if !matches!(self.peek(), Token::RParen) {
8932                        return Err(self.err(format!(
8933                            "expected ')' after SET row values, got {:?}",
8934                            self.peek()
8935                        )));
8936                    }
8937                    self.advance();
8938                    if exprs.len() != cols.len() {
8939                        return Err(self.err(alloc::format!(
8940                            "SET (…) = (…) arity mismatch: {} columns, {} values",
8941                            cols.len(),
8942                            exprs.len()
8943                        )));
8944                    }
8945                    for (col, e) in cols.into_iter().zip(exprs) {
8946                        assignments.push((col, e));
8947                    }
8948                }
8949                if matches!(self.peek(), Token::Comma) {
8950                    self.advance();
8951                    continue;
8952                }
8953                break;
8954            }
8955            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
8956            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
8957            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
8958            // `public.` dump qualifiers), so the qualifier has to be read off
8959            // the token stream first — otherwise `SET b.v = 888` would write
8960            // to the TARGET table's `v` while naming a source table, a
8961            // silent-wrong. A qualifier naming a SOURCE table means a
8962            // multi-TARGET update — mutating two tables in one statement —
8963            // which SPG does not model, so it is refused loudly.
8964            let set_qual: Option<String> = if mysql_from.is_some()
8965                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
8966            {
8967                match self.peek() {
8968                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
8969                    _ => None,
8970                }
8971            } else {
8972                None
8973            };
8974            let col = self.expect_ident_like()?;
8975            if let Some(q) = set_qual {
8976                let names_target = q.eq_ignore_ascii_case(&table)
8977                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
8978                if !names_target {
8979                    return Err(self.err(alloc::format!(
8980                        "multi-table UPDATE can only assign to its first table \
8981                         ({table}); `{q}.{col}` targets another table"
8982                    )));
8983                }
8984            }
8985            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
8986            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
8987            // `__column_default` marker lowering just below). PG assigns to the
8988            // i-th (1-based) element, NULL-padding when i exceeds the length.
8989            if matches!(self.peek(), Token::LBracket) {
8990                self.advance();
8991                let index = self.parse_expr(0)?;
8992                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
8993                // (and the open `arr[lo:]`), lowered to
8994                // `__array_assign_slice`. Only the single-subscript form
8995                // parsed before, so a slice assignment was a syntax error.
8996                let mut slice_hi: Option<Option<Expr>> = None;
8997                if matches!(self.peek(), Token::Colon) {
8998                    self.advance();
8999                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9000                        None
9001                    } else {
9002                        Some(self.parse_expr(0)?)
9003                    });
9004                }
9005                if !matches!(self.peek(), Token::RBracket) {
9006                    return Err(self.err(format!(
9007                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9008                        self.peek()
9009                    )));
9010                }
9011                self.advance();
9012                if !matches!(self.peek(), Token::Eq) {
9013                    return Err(self.err(format!(
9014                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9015                        self.peek()
9016                    )));
9017                }
9018                self.advance();
9019                let value = self.parse_expr(0)?;
9020                // PG merges several subscript writes to the same column into one
9021                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9022                // assignment to `col` rather than each overwriting the original.
9023                let existing = assignments.iter().position(|(c, _)| c == &col);
9024                let base = match existing {
9025                    Some(i) => assignments[i].1.clone(),
9026                    None => Expr::Column(ColumnName {
9027                        qualifier: None,
9028                        name: col.clone(),
9029                    }),
9030                };
9031                let assigned = match slice_hi {
9032                    None => Expr::FunctionCall {
9033                        name: "__array_assign".to_string(),
9034                        args: alloc::vec![base, index, value],
9035                    },
9036                    Some(hi) => Expr::FunctionCall {
9037                        name: "__array_assign_slice".to_string(),
9038                        args: alloc::vec![
9039                            base,
9040                            index,
9041                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9042                            value,
9043                        ],
9044                    },
9045                };
9046                match existing {
9047                    Some(i) => assignments[i].1 = assigned,
9048                    None => assignments.push((col, assigned)),
9049                }
9050                if matches!(self.peek(), Token::Comma) {
9051                    self.advance();
9052                    continue;
9053                }
9054                break;
9055            }
9056            if !matches!(self.peek(), Token::Eq) {
9057                return Err(self.err(format!(
9058                    "expected `=` after column name in UPDATE SET, got {:?}",
9059                    self.peek()
9060                )));
9061            }
9062            self.advance();
9063            // `SET col = DEFAULT` — the column's declared default;
9064            // rides out as a marker call the update executor
9065            // resolves against the schema.
9066            let value = if matches!(self.peek(), Token::Default) {
9067                self.advance();
9068                Expr::FunctionCall {
9069                    name: "__column_default".to_string(),
9070                    args: Vec::new(),
9071                }
9072            } else {
9073                self.parse_expr(0)?
9074            };
9075            assignments.push((col, value));
9076            if matches!(self.peek(), Token::Comma) {
9077                self.advance();
9078                continue;
9079            }
9080            break;
9081        }
9082        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9083        // update. Lowers onto the correlated-subquery machinery:
9084        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9085        // and each assignment that references a FROM-list table
9086        // wraps into a correlated scalar subquery
9087        // (SELECT expr FROM src WHERE cond). Equivalent for the
9088        // unique-join shape (the overwhelmingly common one); a
9089        // multi-match, which PG resolves by arbitrary pick,
9090        // surfaces as a scalar-subquery cardinality error instead
9091        // of a silent arbitrary result.
9092        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9093        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9094        // the SAME lowering below. Both spellings together is not legal in
9095        // either dialect.
9096        let from_clause = if let Some(fc) = mysql_from {
9097            if matches!(self.peek(), Token::From) {
9098                return Err(self.err(alloc::string::String::from(
9099                    "multi-table UPDATE already names its sources; drop the FROM clause",
9100                )));
9101            }
9102            Some(fc)
9103        } else if matches!(self.peek(), Token::From) {
9104            self.advance();
9105            Some(self.parse_from_clause()?)
9106        } else {
9107            None
9108        };
9109        let where_ = if matches!(self.peek(), Token::Where) {
9110            self.advance();
9111            Some(self.parse_expr(0)?)
9112        } else {
9113            None
9114        };
9115        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9116        // and the TARGET-row filter are NOT the same predicate once a LEFT
9117        // join is involved:
9118        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9119        //     one conjunction, and the whole thing filters target rows via
9120        //     EXISTS.
9121        //   * LEFT join: only the ON predicate belongs inside the source
9122        //     subquery. The WHERE still filters TARGET rows (with source
9123        //     columns read through the correlated subquery, which yields NULL
9124        //     for an unmatched row — exactly LEFT-join semantics).
9125        // Round 420 folded ON into WHERE unconditionally and then dropped the
9126        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9127        // WHERE a.id > 1` updated EVERY row.
9128        let sub_where = match (mysql_on.clone(), where_.clone()) {
9129            _ if mysql_outer => mysql_on.clone(),
9130            (Some(on), Some(w)) => Some(Expr::Binary {
9131                lhs: Box::new(on),
9132                op: crate::ast::BinOp::And,
9133                rhs: Box::new(w),
9134            }),
9135            (Some(on), None) => Some(on),
9136            (None, w) => w,
9137        };
9138        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9139        // has no such clause on UPDATE, so this is accepted only under the
9140        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9141        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9142        let mut returning = self.parse_optional_returning()?;
9143        // v7.39 (round 533) — kept for the engine, which can resolve the
9144        // UNQUALIFIED leaves this lowering has to leave alone.
9145        let from_sources = from_clause.as_ref().map(|fc| {
9146            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9147                from: fc.clone(),
9148                sub_where: sub_where.clone(),
9149            })
9150        });
9151        let (assignments, where_) = if let Some(fc) = from_clause {
9152            let names: Vec<String> = core::iter::once(&fc.primary)
9153                .chain(fc.joins.iter().map(|j| &j.table))
9154                .flat_map(|t| {
9155                    t.alias
9156                        .clone()
9157                        .into_iter()
9158                        .chain(core::iter::once(t.name.clone()))
9159                })
9160                .collect();
9161            let refs_list = |e: &Expr| -> bool {
9162                fn walk(e: &Expr, names: &[String]) -> bool {
9163                    match e {
9164                        Expr::Column(c) => c
9165                            .qualifier
9166                            .as_deref()
9167                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9168                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9169                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9170                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9171                        Expr::Case {
9172                            operand,
9173                            branches,
9174                            else_branch,
9175                        } => {
9176                            operand.as_deref().is_some_and(|o| walk(o, names))
9177                                || branches
9178                                    .iter()
9179                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9180                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9181                        }
9182                        _ => false,
9183                    }
9184                }
9185                walk(e, &names)
9186            };
9187            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9188                locking: None,
9189                ctes: Vec::new(),
9190                distinct: false,
9191                distinct_on: Vec::new(),
9192                items,
9193                from: Some(fc.clone()),
9194                where_: sub_where.clone(),
9195                group_by: None,
9196                group_by_all: false,
9197                having: None,
9198                unions: Vec::new(),
9199                order_by: Vec::new(),
9200                limit: None,
9201                offset: None,
9202                limit_with_ties: false,
9203                window_check_exprs: Vec::new(),
9204            };
9205            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9206            // assignment RHS with a correlated scalar subquery, instead of
9207            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9208            // column reference (`SET v = v + u.bonus`, where `v` is the target
9209            // table's column) inside a subquery whose FROM only has the source
9210            // table, so the unqualified `v` resolved against the source and
9211            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9212            // context — where they belong — fixes it; only the source columns
9213            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9214            // compound variants the leaf-walk doesn't decompose.
9215            let make_subq = |inner: Expr| {
9216                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9217                    expr: inner,
9218                    alias: None,
9219                }])))
9220            };
9221            let assignments = assignments
9222                .into_iter()
9223                .map(|(col, mut expr)| {
9224                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9225                    (col, expr)
9226                })
9227                .collect();
9228            let exists = Expr::Exists {
9229                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9230                    expr: Expr::Literal(Literal::Integer(1)),
9231                    alias: None,
9232                }])),
9233                negated: false,
9234            };
9235            // v7.39 (round 241) — RETURNING may reference the FROM-list
9236            // tables too (`RETURNING emp.id, dept.name`); the same
9237            // leaf-to-correlated-subquery lowering the assignments get.
9238            // Without it the qualifier died at eval with "unknown table
9239            // qualifier". (RETURNING was parsed before this block — the
9240            // lowering is a pure AST transformation.)
9241            if let Some(items) = returning.as_mut() {
9242                for item in items.iter_mut() {
9243                    if let SelectItem::Expr { expr, .. } = item {
9244                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9245                    }
9246                }
9247            }
9248            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9249            // EVERY matching target row: it gets no EXISTS filter, but the
9250            // caller's WHERE still applies, with source columns read through
9251            // the correlated subquery (NULL when unmatched — LEFT-join
9252            // semantics). `sub_where` above already excluded the WHERE from
9253            // the source subquery for this case.
9254            if mysql_outer {
9255                let mut outer = where_;
9256                if let Some(w) = outer.as_mut() {
9257                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9258                }
9259                (assignments, outer)
9260            } else {
9261                (assignments, Some(exists))
9262            }
9263        } else {
9264            (assignments, where_)
9265        };
9266        Ok(Statement::Update(crate::ast::UpdateStatement {
9267            ctes: Vec::new(),
9268            table,
9269            only,
9270            alias,
9271            assignments,
9272            from_sources,
9273            where_,
9274            order_limit: update_order_limit,
9275            returning,
9276        }))
9277    }
9278
9279    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9280    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9281    /// clause and its meaning are identical, so both call this rather than
9282    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9283    /// legal. PG has no such clause on either statement, so it is read only
9284    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9285    /// errors.
9286    ///
9287    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9288    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9289    /// stack in round 430.
9290    #[inline(never)]
9291    fn parse_mysql_dml_order_limit(
9292        &mut self,
9293        what: &str,
9294    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9295        if !self.mysql_dialect {
9296            return Ok(None);
9297        }
9298        let order_by = self.parse_order_by_keys()?;
9299        let limit = if matches!(self.peek(), Token::Limit) {
9300            self.advance();
9301            let tok = self.advance();
9302            let Token::Integer(n) = tok else {
9303                return Err(self.err(alloc::format!(
9304                    "expected integer after {what} LIMIT, got {tok:?}"
9305                )));
9306            };
9307            // MySQL rejects the `LIMIT offset, count` form here — only a
9308            // single row count is legal on a DML statement.
9309            if matches!(self.peek(), Token::Comma) {
9310                return Err(self.err(alloc::format!(
9311                    "{what} LIMIT takes a row count, not an offset"
9312                )));
9313            }
9314            let n = u32::try_from(n)
9315                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9316            Some(n)
9317        } else {
9318            None
9319        };
9320        if order_by.is_empty() && limit.is_none() {
9321            return Ok(None);
9322        }
9323        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9324            order_by,
9325            limit,
9326        })))
9327    }
9328
9329    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9330    /// the leading `DELETE` ident.
9331    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9332        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9333        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9334        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9335        // parse here; it reaches the existing USING path with the target
9336        // repeated in the list, which the source-list peel below handles.)
9337        // More than one name is a multi-TARGET delete, which SPG does not
9338        // model; it is refused rather than half-applied.
9339        let mysql_pre_target: Option<String> =
9340            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9341                let first = self.expect_ident_like()?;
9342                if matches!(self.peek(), Token::Comma) {
9343                    return Err(self.err(alloc::format!(
9344                        "multi-table DELETE can only delete from one table; \
9345                     `DELETE {first}, …` names several"
9346                    )));
9347                }
9348                Some(first)
9349            } else {
9350                None
9351            };
9352        if !matches!(self.peek(), Token::From) {
9353            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9354        }
9355        self.advance();
9356        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9357        // lookahead as the UPDATE spelling.
9358        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9359            if s.eq_ignore_ascii_case("only"))
9360            && matches!(
9361                self.tokens.get(self.pos + 1),
9362                Some(Token::Ident(_) | Token::QuotedIdent(_))
9363            );
9364        if only {
9365            self.advance();
9366        }
9367        let table = self.expect_ident_like()?;
9368        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9369        // spelling must not swallow the clause keywords that can follow
9370        // the target.
9371        let alias = if matches!(self.peek(), Token::As) {
9372            self.advance();
9373            Some(self.expect_ident_like()?)
9374        } else {
9375            match self.peek() {
9376                Token::Ident(s) | Token::QuotedIdent(s)
9377                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9378                {
9379                    let a = s.clone();
9380                    self.advance();
9381                    Some(a)
9382                }
9383                _ => None,
9384            }
9385        };
9386        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9387        // through the SAME join grammar the FROM clause uses (see the
9388        // `advance()`-destroys-tokens note on `parse_from_joins`).
9389        let mut mysql_on: Option<Expr> = None;
9390        let mut mysql_outer = false;
9391        let mysql_using = if mysql_pre_target.is_some()
9392            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9393        {
9394            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9395            let mut joins = self.parse_from_joins(&target_qual)?;
9396            if joins.is_empty() {
9397                return Err(self.err(alloc::string::String::from(
9398                    "multi-table DELETE needs at least one source table",
9399                )));
9400            }
9401            let head = joins.remove(0);
9402            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9403            mysql_on = head.on;
9404            Some(FromClause {
9405                primary: head.table,
9406                joins,
9407            })
9408        } else {
9409            None
9410        };
9411        // The pre-FROM target must be the table the FROM names (or its
9412        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9413        // is not the scan target.
9414        if let Some(t) = &mysql_pre_target {
9415            let names_target = t.eq_ignore_ascii_case(&table)
9416                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9417            if !names_target {
9418                return Err(self.err(alloc::format!(
9419                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9420                )));
9421            }
9422        }
9423        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9424        // delete. Same lowering as UPDATE … FROM: the WHERE
9425        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9426        // target row by the correlated machinery.
9427        let using_clause = if let Some(fc) = mysql_using {
9428            Some(fc)
9429        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9430            self.advance();
9431            let mut fc = self.parse_from_clause()?;
9432            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9433            // repeats the TARGET as the first USING entry (PG's spelling
9434            // lists only the extra sources). Peel it so the source subquery
9435            // does not re-scan — and shadow — the target table.
9436            let primary_is_target =
9437                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9438            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9439                let head = fc.joins.remove(0);
9440                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9441                mysql_on = head.on;
9442                fc = FromClause {
9443                    primary: head.table,
9444                    joins: fc.joins,
9445                };
9446            }
9447            Some(fc)
9448        } else {
9449            None
9450        };
9451        let where_ = if matches!(self.peek(), Token::Where) {
9452            self.advance();
9453            Some(self.parse_expr(0)?)
9454        } else {
9455            None
9456        };
9457        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9458        // read before RETURNING (MariaDB's own extension trails the LIMIT).
9459        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9460        let mut returning = self.parse_optional_returning()?;
9461        let where_ = if let Some(fc) = using_clause {
9462            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9463            // a USING-table reference in RETURNING becomes a correlated
9464            // scalar subquery over the USING list.
9465            let names: Vec<String> = core::iter::once(&fc.primary)
9466                .chain(fc.joins.iter().map(|j| &j.table))
9467                .flat_map(|t| {
9468                    t.alias
9469                        .clone()
9470                        .into_iter()
9471                        .chain(core::iter::once(t.name.clone()))
9472                })
9473                .collect();
9474            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9475            // join filters the SOURCE subquery on the ON predicate alone and
9476            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9477            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9478            // rows); every other form folds ON and WHERE into one EXISTS.
9479            let sub_where = match (mysql_on.clone(), where_.clone()) {
9480                _ if mysql_outer => mysql_on.clone(),
9481                (Some(on), Some(w)) => Some(Expr::Binary {
9482                    lhs: Box::new(on),
9483                    op: crate::ast::BinOp::And,
9484                    rhs: Box::new(w),
9485                }),
9486                (Some(on), None) => Some(on),
9487                (None, w) => w,
9488            };
9489            let exists_where = sub_where.clone();
9490            let sub_fc = fc.clone();
9491            let make_subq = move |leaf: Expr| -> Expr {
9492                Expr::ScalarSubquery(Box::new(SelectStatement {
9493                    locking: None,
9494                    ctes: Vec::new(),
9495                    distinct: false,
9496                    distinct_on: Vec::new(),
9497                    items: alloc::vec![SelectItem::Expr {
9498                        expr: leaf,
9499                        alias: None,
9500                    }],
9501                    from: Some(sub_fc.clone()),
9502                    where_: sub_where.clone(),
9503                    group_by: None,
9504                    group_by_all: false,
9505                    having: None,
9506                    unions: Vec::new(),
9507                    order_by: Vec::new(),
9508                    limit: None,
9509                    offset: None,
9510                    limit_with_ties: false,
9511                    window_check_exprs: Vec::new(),
9512                }))
9513            };
9514            let refs = |e: &Expr| expr_refs_tables(e, &names);
9515            if let Some(items) = returning.as_mut() {
9516                for item in items.iter_mut() {
9517                    if let SelectItem::Expr { expr, .. } = item {
9518                        wrap_from_leaves(expr, &names, &make_subq, &refs);
9519                    }
9520                }
9521            }
9522            // A LEFT join deletes the target rows the WHERE selects, reading
9523            // source columns through the correlated subquery (NULL when
9524            // unmatched); no EXISTS row filter.
9525            if mysql_outer {
9526                let mut outer = where_;
9527                if let Some(w) = outer.as_mut() {
9528                    wrap_from_leaves(w, &names, &make_subq, &refs);
9529                }
9530                outer
9531            } else {
9532                Some(Expr::Exists {
9533                    subquery: Box::new(SelectStatement {
9534                        locking: None,
9535                        ctes: Vec::new(),
9536                        distinct: false,
9537                        distinct_on: Vec::new(),
9538                        items: alloc::vec![SelectItem::Expr {
9539                            expr: Expr::Literal(Literal::Integer(1)),
9540                            alias: None,
9541                        }],
9542                        from: Some(fc),
9543                        where_: exists_where,
9544                        group_by: None,
9545                        group_by_all: false,
9546                        having: None,
9547                        unions: Vec::new(),
9548                        order_by: Vec::new(),
9549                        limit: None,
9550                        offset: None,
9551                        limit_with_ties: false,
9552                        window_check_exprs: Vec::new(),
9553                    }),
9554                    negated: false,
9555                })
9556            }
9557        } else {
9558            where_
9559        };
9560        Ok(Statement::Delete(crate::ast::DeleteStatement {
9561            ctes: Vec::new(),
9562            table,
9563            only,
9564            alias,
9565            where_,
9566            order_limit: delete_order_limit,
9567            returning,
9568        }))
9569    }
9570
9571    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9572    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9573    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9574    /// keyword. v7.17 surface:
9575    ///   * source: table reference (subquery source is a follow-up)
9576    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9577    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9578    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
9579    ///     order
9580    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9581        // INTO
9582        let is_into_kw = matches!(self.peek(), Token::Into)
9583            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9584        if !is_into_kw {
9585            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9586        }
9587        self.advance();
9588        let target = self.expect_ident_like()?;
9589        // Optional alias — bare ident before USING.
9590        let target_alias = match self.peek() {
9591            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9592                Some(self.expect_ident_like()?)
9593            }
9594            _ => None,
9595        };
9596        // USING
9597        let is_using_kw = matches!(
9598            self.peek(),
9599            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9600        );
9601        if !is_using_kw {
9602            return Err(self.err(format!(
9603                "expected USING after MERGE INTO target, got {:?}",
9604                self.peek()
9605            )));
9606        }
9607        self.advance();
9608        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9609        // <table> [alias]`. PG requires an alias after a subquery source.
9610        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9611            self.advance(); // (
9612            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9613            // constant-SELECT lowering the derived-table parser uses
9614            // (PG deletes through this form; it was a parse error).
9615            let inner = if matches!(self.peek(), Token::Values) {
9616                self.advance(); // VALUES
9617                Statement::Select(self.parse_values_rows_body()?)
9618            } else {
9619                self.parse_select_stmt()?
9620            };
9621            match self.advance() {
9622                Token::RParen => {}
9623                other => {
9624                    return Err(self.err(format!(
9625                        "expected ')' after MERGE USING subquery, got {other:?}"
9626                    )));
9627                }
9628            }
9629            let Statement::Select(sub) = inner else {
9630                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9631            };
9632            (String::new(), Some(Box::new(sub)))
9633        } else {
9634            (self.expect_ident_like()?, None)
9635        };
9636        let source_alias = match self.peek() {
9637            Token::Ident(s) | Token::QuotedIdent(s)
9638                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9639            {
9640                Some(self.expect_ident_like()?)
9641            }
9642            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9643                self.advance(); // AS
9644                Some(self.expect_ident_like()?)
9645            }
9646            _ => None,
9647        };
9648        // v7.39 (round 768, F31-D5) — optional positional column-alias
9649        // list after the source alias (`s(id, v)`).
9650        let mut source_column_aliases: Vec<String> = Vec::new();
9651        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9652            self.advance();
9653            loop {
9654                source_column_aliases.push(self.expect_ident_like()?);
9655                match self.peek() {
9656                    Token::Comma => {
9657                        self.advance();
9658                    }
9659                    Token::RParen => {
9660                        self.advance();
9661                        break;
9662                    }
9663                    other => {
9664                        return Err(self.err(format!(
9665                            "expected ',' or ')' in MERGE source column list, got {other:?}"
9666                        )));
9667                    }
9668                }
9669            }
9670        }
9671        if source_select.is_some() && source_alias.is_none() {
9672            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9673        }
9674        // ON
9675        if !matches!(self.peek(), Token::On) {
9676            return Err(self.err(format!(
9677                "expected ON after MERGE … USING source, got {:?}",
9678                self.peek()
9679            )));
9680        }
9681        self.advance();
9682        let on = self.parse_expr(0)?;
9683        // One or more WHEN clauses.
9684        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9685        loop {
9686            let is_when_kw = matches!(
9687                self.peek(),
9688                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9689            );
9690            if !is_when_kw {
9691                break;
9692            }
9693            self.advance(); // WHEN
9694            // [NOT] MATCHED
9695            let matched = if matches!(self.peek(), Token::Not) {
9696                self.advance();
9697                crate::ast::MergeMatched::NotMatched
9698            } else {
9699                crate::ast::MergeMatched::Matched
9700            };
9701            let is_matched_kw = matches!(
9702                self.peek(),
9703                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9704            );
9705            if !is_matched_kw {
9706                return Err(self.err(format!(
9707                    "expected MATCHED in WHEN clause, got {:?}",
9708                    self.peek()
9709                )));
9710            }
9711            self.advance();
9712            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9713            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9714            // to fire for target rows no source row matches.
9715            let mut matched = matched;
9716            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9717                self.advance();
9718                match self.peek() {
9719                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9720                        self.advance();
9721                        matched = crate::ast::MergeMatched::NotMatchedBySource;
9722                    }
9723                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9724                        self.advance();
9725                    }
9726                    other => {
9727                        return Err(self.err(format!(
9728                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9729                        )));
9730                    }
9731                }
9732            }
9733            // Optional AND <expr>
9734            let condition = if matches!(self.peek(), Token::And) {
9735                self.advance();
9736                Some(self.parse_expr(0)?)
9737            } else {
9738                None
9739            };
9740            // THEN
9741            let is_then_kw = matches!(
9742                self.peek(),
9743                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
9744            );
9745            if !is_then_kw {
9746                return Err(self.err(format!(
9747                    "expected THEN in WHEN clause, got {:?}",
9748                    self.peek()
9749                )));
9750            }
9751            self.advance();
9752            // Action: INSERT / UPDATE / DELETE / DO NOTHING
9753            let action = match self.peek().clone() {
9754                Token::Insert => {
9755                    self.advance();
9756                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
9757                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
9758                    // VALUES (…)` omits it and fills every column in declaration
9759                    // order. PG accepts this; SPG used to require the list.
9760                    let mut columns: Vec<String> = Vec::new();
9761                    if matches!(self.peek(), Token::LParen) {
9762                        self.advance();
9763                        loop {
9764                            columns.push(self.expect_ident_like()?);
9765                            if matches!(self.peek(), Token::Comma) {
9766                                self.advance();
9767                                continue;
9768                            }
9769                            break;
9770                        }
9771                        if !matches!(self.peek(), Token::RParen) {
9772                            return Err(self.err(format!(
9773                                "expected ')' after INSERT column list, got {:?}",
9774                                self.peek()
9775                            )));
9776                        }
9777                        self.advance();
9778                    }
9779                    // VALUES (...)
9780                    if !matches!(self.peek(), Token::Values) {
9781                        return Err(self.err(format!(
9782                            "expected VALUES in MERGE INSERT, got {:?}",
9783                            self.peek()
9784                        )));
9785                    }
9786                    self.advance();
9787                    if !matches!(self.peek(), Token::LParen) {
9788                        return Err(self.err(format!(
9789                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
9790                            self.peek()
9791                        )));
9792                    }
9793                    self.advance();
9794                    let mut values: Vec<crate::ast::Expr> = Vec::new();
9795                    loop {
9796                        values.push(self.parse_expr(0)?);
9797                        if matches!(self.peek(), Token::Comma) {
9798                            self.advance();
9799                            continue;
9800                        }
9801                        break;
9802                    }
9803                    if !matches!(self.peek(), Token::RParen) {
9804                        return Err(self.err(format!(
9805                            "expected ')' after MERGE INSERT values, got {:?}",
9806                            self.peek()
9807                        )));
9808                    }
9809                    self.advance();
9810                    // Empty column list = positional into every column, so the
9811                    // count is checked against the table arity at execution.
9812                    if !columns.is_empty() && columns.len() != values.len() {
9813                        return Err(self.err(format!(
9814                            "MERGE INSERT column count ({}) ≠ value count ({})",
9815                            columns.len(),
9816                            values.len()
9817                        )));
9818                    }
9819                    crate::ast::MergeAction::Insert { columns, values }
9820                }
9821                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
9822                    self.advance();
9823                    // SET
9824                    let is_set_kw = matches!(
9825                        self.peek(),
9826                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
9827                    );
9828                    if !is_set_kw {
9829                        return Err(self.err(format!(
9830                            "expected SET after UPDATE in MERGE, got {:?}",
9831                            self.peek()
9832                        )));
9833                    }
9834                    self.advance();
9835                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
9836                    loop {
9837                        let col = self.expect_ident_like()?;
9838                        if !matches!(self.peek(), Token::Eq) {
9839                            return Err(self.err(format!(
9840                                "expected '=' in MERGE UPDATE assignment, got {:?}",
9841                                self.peek()
9842                            )));
9843                        }
9844                        self.advance();
9845                        let expr = self.parse_expr(0)?;
9846                        assignments.push((col, expr));
9847                        if matches!(self.peek(), Token::Comma) {
9848                            self.advance();
9849                            continue;
9850                        }
9851                        break;
9852                    }
9853                    crate::ast::MergeAction::Update { assignments }
9854                }
9855                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
9856                    self.advance();
9857                    crate::ast::MergeAction::Delete
9858                }
9859                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
9860                    self.advance();
9861                    let is_nothing_kw = matches!(
9862                        self.peek(),
9863                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
9864                    );
9865                    if !is_nothing_kw {
9866                        return Err(self.err(format!(
9867                            "expected NOTHING after DO in MERGE clause, got {:?}",
9868                            self.peek()
9869                        )));
9870                    }
9871                    self.advance();
9872                    crate::ast::MergeAction::DoNothing
9873                }
9874                other => {
9875                    return Err(self.err(format!(
9876                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
9877                    )));
9878                }
9879            };
9880            // PG's grammar simply has no INSERT production under BY SOURCE
9881            // (a target row already exists there) — same syntax error.
9882            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
9883                && matches!(action, crate::ast::MergeAction::Insert { .. })
9884            {
9885                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
9886            }
9887            clauses.push(crate::ast::MergeWhenClause {
9888                matched,
9889                condition,
9890                action,
9891            });
9892        }
9893        if clauses.is_empty() {
9894            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
9895        }
9896        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
9897        // unconditional (no `AND`) WHEN of the same match kind: it could
9898        // never fire. Check per match kind in clause order.
9899        let mut seen_unconditional_matched = false;
9900        let mut seen_unconditional_not_matched = false;
9901        let mut seen_unconditional_by_source = false;
9902        for c in &clauses {
9903            let seen = match c.matched {
9904                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
9905                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
9906                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
9907            };
9908            if *seen {
9909                return Err(self.err(String::from(
9910                    "unreachable WHEN clause specified after unconditional WHEN clause",
9911                )));
9912            }
9913            if c.condition.is_none() {
9914                *seen = true;
9915            }
9916        }
9917        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
9918        let returning = self.parse_optional_returning()?;
9919        Ok(Statement::Merge(crate::ast::MergeStatement {
9920            // Attached by `parse_with_cte_then_select` when the MERGE
9921            // heads a WITH clause (round 149).
9922            ctes: Vec::new(),
9923            target,
9924            target_alias,
9925            source,
9926            source_alias,
9927            source_select,
9928            source_column_aliases,
9929            on,
9930            clauses,
9931            returning,
9932        }))
9933    }
9934
9935    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
9936    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
9937    /// as SELECT, so `RETURNING *`, `RETURNING col`,
9938    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
9939    fn parse_optional_returning(
9940        &mut self,
9941    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
9942        let is_returning_kw = matches!(
9943            self.peek(),
9944            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
9945        );
9946        if !is_returning_kw {
9947            return Ok(None);
9948        }
9949        self.advance();
9950        let mut items = Vec::new();
9951        loop {
9952            items.push(self.parse_select_item()?);
9953            if matches!(self.peek(), Token::Comma) {
9954                self.advance();
9955                continue;
9956            }
9957            break;
9958        }
9959        Ok(Some(items))
9960    }
9961
9962    /// v6.0.4 — parse the tail of an ALTER statement after the
9963    /// leading `ALTER` keyword has been consumed. Only one form is
9964    /// supported in v6.0.4:
9965    ///
9966    /// ```text
9967    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
9968    /// ```
9969    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
9970        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
9971        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
9972        // exclusion) is accepted by stripping the `ONLY` keyword
9973        // before the table parse.
9974        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
9975        // and the long PG-dump tail are accepted as no-ops.
9976        match self.advance() {
9977            Token::Index => {}
9978            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
9979            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
9980            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
9981            Token::Table => {
9982                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
9983                    self.advance();
9984                }
9985                return self.parse_alter_table_after_keyword();
9986            }
9987            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
9988                return self.parse_alter_policy_after_keyword();
9989            }
9990            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
9991                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
9992                    self.advance();
9993                }
9994                return self.parse_alter_table_after_keyword();
9995            }
9996            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
9997            // of the silent-noop tail.
9998            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
9999                return self.parse_alter_sequence_after_keyword();
10000            }
10001            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10002            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10003            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10004            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10005                // NB: the match arm consumed `TYPE` via self.advance(); the
10006                // cursor is now at the type name — do NOT advance again.
10007                let type_name = self.expect_ident_like()?;
10008                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10009                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10010                if is_add_value {
10011                    self.advance(); // ADD
10012                    self.advance(); // VALUE
10013                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10014                    // IF/EXISTS as identifiers.
10015                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10016                    {
10017                        let n1 = self.tokens.get(self.pos + 1);
10018                        let n2 = self.tokens.get(self.pos + 2);
10019                        if matches!(n1, Some(Token::Not))
10020                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10021                        {
10022                            self.advance();
10023                            self.advance();
10024                            self.advance();
10025                            true
10026                        } else {
10027                            false
10028                        }
10029                    } else {
10030                        false
10031                    };
10032                    let label = self.expect_string_literal()?;
10033                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10034                    {
10035                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10036                        self.advance();
10037                        let anchor = self.expect_string_literal()?;
10038                        Some((is_before, anchor))
10039                    } else {
10040                        None
10041                    };
10042                    return Ok(Statement::AlterTypeAddValue {
10043                        type_name,
10044                        label,
10045                        if_not_exists,
10046                        position,
10047                    });
10048                }
10049                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10050                // Used to fall into the no-op tail below: accepted, silently
10051                // ignored. `RENAME TO <newtype>` keeps falling through.
10052                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10053                    && matches!(
10054                        self.tokens.get(self.pos + 1),
10055                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10056                    )
10057                {
10058                    self.advance(); // RENAME
10059                    self.advance(); // VALUE
10060                    let old = self.expect_string_literal()?;
10061                    if matches!(self.peek(), Token::To) {
10062                        self.advance();
10063                    } else {
10064                        self.expect_keyword_ident("to")?;
10065                    }
10066                    let new = self.expect_string_literal()?;
10067                    return Ok(Statement::AlterTypeRenameValue {
10068                        type_name,
10069                        old,
10070                        new,
10071                    });
10072                }
10073                // Other ALTER TYPE forms — the ACTION stays a no-op
10074                // (pg_dump tail), but v7.39 (round 708) the NAME is
10075                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10076                // success for a type that does not exist.
10077                self.consume_until_statement_boundary();
10078                return Ok(Statement::ValidateOnly {
10079                    kind: crate::ast::ValidateOnlyKind::TypeName,
10080                    names: alloc::vec![type_name],
10081                });
10082            }
10083            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10084            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10085            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10086            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10087            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10088            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10089            // pg_dump no-op list below: every form used to report success
10090            // and change nothing, which is worse than refusing outright
10091            // (a migration dropping a constraint kept rejecting data).
10092            // NOTE: the enclosing `match self.advance()` already consumed
10093            // the DOMAIN keyword, so the name is next.
10094            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10095                return self.parse_alter_domain_after_keyword();
10096            }
10097            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10098            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10099            // used to fall into the pg_dump no-op tail below, so a DBA
10100            // setting a per-role default was told it worked and nothing
10101            // happened. Intercepted here, BEFORE that tail.
10102            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10103            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10104            // interception below exists: swallowed with the no-op tail, an
10105            // unknown parameter name was ACCEPTED where PG18 answers
10106            // `unrecognized configuration parameter`. SPG applies nothing
10107            // either way — there is no postgresql.auto.conf — but it now
10108            // says so about a name it does not know.
10109            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10110                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10111                // already consumed here. An extra advance eats the SET and
10112                // the parameter name is never seen — which is exactly the
10113                // bug a panic in this branch disproved: the branch WAS on
10114                // the path, the reading of it was wrong.
10115                let mut parameter = None;
10116                // SET <name> … | RESET <name> | RESET ALL
10117                if matches!(self.peek(), Token::Ident(k)
10118                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10119                {
10120                    self.advance();
10121                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10122                        && !n.eq_ignore_ascii_case("all")
10123                    {
10124                        self.advance();
10125                        // A dotted GUC (`plpgsql.check_asserts`) is two
10126                        // tokens; keep the whole name.
10127                        let mut full = n;
10128                        while matches!(self.peek(), Token::Dot) {
10129                            self.advance();
10130                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10131                                full.push('.');
10132                                full.push_str(&t);
10133                            }
10134                        }
10135                        parameter = Some(full);
10136                    }
10137                }
10138                self.consume_until_statement_boundary();
10139                return Ok(Statement::AlterSystem { parameter });
10140            }
10141            Token::Ident(s) | Token::QuotedIdent(s)
10142                if matches!(
10143                    s.to_ascii_lowercase().as_str(),
10144                    "role" | "user" | "database"
10145                ) && self.peeks_db_role_setting() =>
10146            {
10147                let is_database = s.eq_ignore_ascii_case("database");
10148                return self.parse_db_role_setting(is_database);
10149            }
10150            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10151            // (the non-SET forms; SET/RESET took the branch above). The
10152            // attributes still no-op — recorded, and the ignored PASSWORD
10153            // is ledgered as its own follow-up — but the ROLE is validated:
10154            // any name was accepted for a role that does not exist.
10155            Token::Ident(s) | Token::QuotedIdent(s)
10156                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10157            {
10158                // NB: the enclosing `match self.advance()` already consumed
10159                // ROLE/USER — the round-695 trap, hit again in this round's
10160                // first draft (the name was eaten and WITH parsed as the
10161                // role). The cursor is at the name.
10162                let name = self.expect_ident_or_string()?;
10163                // v7.39 (round 750) — scan the attribute tail for
10164                // PASSWORD. Everything else stays a recorded no-op, but
10165                // a dropped credential rotation is a SECURITY bug:
10166                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10167                // changed nothing, so the old password kept working.
10168                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10169                // NULL` clears the credential.
10170                let mut password: Option<Option<String>> = None;
10171                loop {
10172                    match self.peek() {
10173                        Token::Semicolon | Token::Eof => break,
10174                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10175                            self.advance();
10176                            match self.advance() {
10177                                Token::String(p) => password = Some(Some(p)),
10178                                Token::Null => password = Some(None),
10179                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10180                                    password = Some(None);
10181                                }
10182                                other => {
10183                                    return Err(self.err(alloc::format!(
10184                                        "expected password string or NULL after PASSWORD, got {other:?}"
10185                                    )));
10186                                }
10187                            }
10188                        }
10189                        _ => {
10190                            self.advance();
10191                        }
10192                    }
10193                }
10194                if name.eq_ignore_ascii_case("all") {
10195                    // `ALTER ROLE ALL …` names every role; nothing to check.
10196                    return Ok(Statement::Empty);
10197                }
10198                if let Some(pw) = password {
10199                    return Ok(Statement::AlterRolePassword { name, password: pw });
10200                }
10201                return Ok(Statement::ValidateOnly {
10202                    kind: crate::ast::ValidateOnlyKind::RoleName,
10203                    names: alloc::vec![name],
10204                });
10205            }
10206            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10207            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10208            // list far enough to validate the NAME; the actions still no-op.
10209            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10210            // models none of them and their dumps are rare.)
10211            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10212                let name = self.expect_ident_or_string()?;
10213                self.consume_until_statement_boundary();
10214                return Ok(Statement::ValidateOnly {
10215                    kind: crate::ast::ValidateOnlyKind::CollationName,
10216                    names: alloc::vec![name],
10217                });
10218            }
10219            Token::Ident(s) | Token::QuotedIdent(s)
10220                if s.eq_ignore_ascii_case("text")
10221                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10222                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10223            {
10224                self.advance(); // SEARCH
10225                self.advance(); // CONFIGURATION
10226                let name = self.expect_ident_like()?;
10227                self.consume_until_statement_boundary();
10228                return Ok(Statement::ValidateOnly {
10229                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10230                    names: alloc::vec![name],
10231                });
10232            }
10233            Token::Ident(s) | Token::QuotedIdent(s)
10234                if s.eq_ignore_ascii_case("event")
10235                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10236            {
10237                self.advance(); // TRIGGER
10238                let name = self.expect_ident_like()?;
10239                self.consume_until_statement_boundary();
10240                return Ok(Statement::ValidateOnly {
10241                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10242                    names: alloc::vec![name],
10243                });
10244            }
10245            Token::Ident(s) | Token::QuotedIdent(s)
10246                if s.eq_ignore_ascii_case("large")
10247                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10248            {
10249                self.advance(); // OBJECT
10250                let oid = match self.advance() {
10251                    Token::Integer(n) => alloc::format!("{n}"),
10252                    other => {
10253                        return Err(
10254                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10255                        );
10256                    }
10257                };
10258                self.consume_until_statement_boundary();
10259                return Ok(Statement::ValidateOnly {
10260                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10261                    names: alloc::vec![oid],
10262                });
10263            }
10264            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10265            // argument-list parse as DROP AGGREGATE (round 707); the
10266            // action no-ops, the existence check is real.
10267            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10268                // Same round-695 trap as above: AGGREGATE is already
10269                // consumed; the cursor is at the name.
10270                let name = self.expect_ident_like()?;
10271                let mut names = alloc::vec![name];
10272                if matches!(self.peek(), Token::LParen) {
10273                    self.advance();
10274                    loop {
10275                        match self.peek().clone() {
10276                            Token::RParen => {
10277                                self.advance();
10278                                break;
10279                            }
10280                            Token::Star => {
10281                                self.advance();
10282                                names.push(String::from("*"));
10283                            }
10284                            Token::Comma => {
10285                                self.advance();
10286                            }
10287                            _ => {
10288                                let mut t = self.expect_ident_like()?;
10289                                while let Token::Ident(nx) = self.peek() {
10290                                    let nx = nx.clone();
10291                                    self.advance();
10292                                    t.push(' ');
10293                                    t.push_str(&nx);
10294                                }
10295                                names.push(t);
10296                            }
10297                        }
10298                    }
10299                }
10300                self.consume_until_statement_boundary();
10301                return Ok(Statement::ValidateOnly {
10302                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10303                    names,
10304                });
10305            }
10306            Token::Ident(s) | Token::QuotedIdent(s)
10307                if matches!(
10308                    s.to_ascii_lowercase().as_str(),
10309                    "view"
10310                        | "function"
10311                        | "database"
10312                        | "schema"
10313                        | "owner"
10314                        | "default"
10315                        | "extension"
10316                        | "materialized"
10317                        | "publication"
10318                        | "subscription"
10319                        // v7.37.17 (17.6 siblings) — additional ALTER
10320                        // targets pg_dump / pg_dumpall / operator DB
10321                        // migration scripts commonly emit. SPG has
10322                        // no matching machinery for any of these; the
10323                        // parser accepts + Empty-returns so pg_dump
10324                        // tail statements don't stall.
10325                        | "tablespace"
10326                        | "language"
10327                        | "operator"
10328                        | "conversion"
10329                        | "statistics"
10330                        | "server"
10331                        | "foreign"
10332                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10333                        // / TEMPLATE (CONFIGURATION intercepted above).
10334                        | "text"
10335                ) =>
10336            {
10337                self.consume_until_statement_boundary();
10338                return Ok(Statement::Empty);
10339            }
10340            other => {
10341                return Err(self.err(format!(
10342                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10343                     after ALTER, got {other:?}"
10344                )));
10345            }
10346        }
10347        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10348        // (mailrs migrate-042 ships these). The presence of an
10349        // IF EXISTS makes the subsequent name lookup tolerate
10350        // a missing index — engine returns CommandOk no-op.
10351        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10352            let next = self.tokens.get(self.pos + 1);
10353            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10354                self.advance();
10355                self.advance();
10356                true
10357            } else {
10358                false
10359            }
10360        } else {
10361            false
10362        };
10363        let name = self.expect_ident_like()?;
10364        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10365        // Detect BEFORE the REBUILD path so the existing REBUILD
10366        // arm stays untouched.
10367        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10368            self.advance();
10369            if matches!(self.peek(), Token::To) {
10370                self.advance();
10371            } else {
10372                self.expect_keyword_ident("to")?;
10373            }
10374            let new = self.expect_ident_like()?;
10375            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10376                name,
10377                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10378            }));
10379        }
10380        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10381        // A syntax error before; the index is validated, the params no-op.
10382        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10383            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10384                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10385        {
10386            self.consume_until_statement_boundary();
10387            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10388                name,
10389                target: crate::ast::AlterIndexTarget::StorageParams,
10390            }));
10391        }
10392        // REBUILD
10393        self.expect_keyword_ident("rebuild")?;
10394        // Optional: WITH (encoding = <enc>)
10395        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10396            self.advance();
10397            if !matches!(self.peek(), Token::LParen) {
10398                return Err(self.err(format!(
10399                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10400                    self.peek()
10401                )));
10402            }
10403            self.advance();
10404            self.expect_keyword_ident("encoding")?;
10405            if !matches!(self.peek(), Token::Eq) {
10406                return Err(self.err(format!(
10407                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10408                    self.peek()
10409                )));
10410            }
10411            self.advance();
10412            let enc_ident = match self.advance() {
10413                Token::Ident(s) | Token::QuotedIdent(s) => s,
10414                other => {
10415                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10416                }
10417            };
10418            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10419                "f32" => VecEncoding::F32,
10420                "sq8" => VecEncoding::Sq8,
10421                "half" => VecEncoding::F16,
10422                other => {
10423                    return Err(self.err(format!(
10424                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10425                    )));
10426                }
10427            };
10428            if !matches!(self.peek(), Token::RParen) {
10429                return Err(self.err(format!(
10430                    "expected ')' after encoding value, got {:?}",
10431                    self.peek()
10432                )));
10433            }
10434            self.advance();
10435            Some(enc)
10436        } else {
10437            None
10438        };
10439        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10440            name,
10441            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10442        }))
10443    }
10444
10445    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10446    /// only `SET` form currently supported; future v6.7.x can add
10447    /// more SET subjects without changing the dispatch shape.
10448    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10449    /// subactions. Single-subaction shape stays a 1-element vec.
10450    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10451        let table_name = self.expect_ident_like()?;
10452        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10453        loop {
10454            let subaction = self.parse_alter_table_subaction()?;
10455            // ADD COLUMN with inline REFERENCES emits both an
10456            // AddColumn and an AddForeignKey subaction; the
10457            // helper returns 1 or 2 items.
10458            targets.extend(subaction);
10459            if matches!(self.peek(), Token::Comma) {
10460                self.advance();
10461                continue;
10462            }
10463            break;
10464        }
10465        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10466            name: table_name,
10467            targets,
10468        }))
10469    }
10470
10471    /// Parse one ALTER TABLE subaction. Returns a Vec because
10472    /// inline `REFERENCES` on `ADD COLUMN` produces both an
10473    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10474    fn parse_alter_table_subaction(
10475        &mut self,
10476    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10477        match self.peek() {
10478            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10479                self.advance();
10480                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10481                // storage parameters: paren-prefixed; consume.
10482                if matches!(self.peek(), Token::LParen) {
10483                    self.consume_until_statement_boundary();
10484                    return Ok(Vec::new());
10485                }
10486                let setting = self.expect_ident_like()?;
10487                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10488                    if !matches!(self.peek(), Token::Eq) {
10489                        return Err(self.err(alloc::format!(
10490                            "expected '=' after hot_tier_bytes, got {:?}",
10491                            self.peek()
10492                        )));
10493                    }
10494                    self.advance();
10495                    let n = self.expect_u64_literal()?;
10496                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10497                }
10498                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10499                // accept-and-no-op for ALTER TABLE SET <subject>
10500                // forms that pg_dump emits but SPG either treats
10501                // as N/A (single-tenant, single-owner, no shared
10502                // tablespaces) or accepts the dump-side declaration
10503                // without runtime effect:
10504                //   SET SCHEMA <name>            (18.11)
10505                //   SET TABLESPACE <name>        (18.8)
10506                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
10507                //   SET WITHOUT CLUSTER          (18.13)
10508                //   SET WITHOUT OIDS             (PG legacy)
10509                //   SET (option = value, …)      (storage parameters)
10510                //   SET REPLICA IDENTITY {…}     (18.14)
10511                if setting.eq_ignore_ascii_case("schema")
10512                    || setting.eq_ignore_ascii_case("tablespace")
10513                    || setting.eq_ignore_ascii_case("logged")
10514                    || setting.eq_ignore_ascii_case("unlogged")
10515                    || setting.eq_ignore_ascii_case("without")
10516                {
10517                    self.consume_until_statement_boundary();
10518                    return Ok(Vec::new());
10519                }
10520                if setting.eq_ignore_ascii_case("replica") {
10521                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10522                    self.consume_until_statement_boundary();
10523                    return Ok(Vec::new());
10524                }
10525                // SET (option=value, …) — storage parameters.
10526                if matches!(self.peek(), Token::LParen) {
10527                    self.consume_until_statement_boundary();
10528                    return Ok(Vec::new());
10529                }
10530                Err(self.err(alloc::format!(
10531                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10532                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10533                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10534                )))
10535            }
10536            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10537            // not ignored: round 645 gave SPG the inheritance the
10538            // v7.37.18 no-op said it did not have.
10539            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10540                self.advance();
10541                let parent = self.expect_ident_like()?;
10542                self.consume_until_statement_boundary();
10543                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10544                    parent,
10545                    detach: false
10546                }])
10547            }
10548            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10549            // LEVEL SECURITY`, which has its own RLS arm below — without
10550            // the guard this swallowed NO FORCE as a no-op.
10551            Token::Ident(s)
10552                if s.eq_ignore_ascii_case("no")
10553                    && !matches!(
10554                        self.tokens.get(self.pos + 1),
10555                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10556                    ) =>
10557            {
10558                self.advance();
10559                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10560                    if k.eq_ignore_ascii_case("inherit"))
10561                {
10562                    self.advance();
10563                    let parent = self.expect_ident_like()?;
10564                    self.consume_until_statement_boundary();
10565                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10566                        parent,
10567                        detach: true
10568                    }]);
10569                }
10570                self.consume_until_statement_boundary();
10571                Ok(Vec::new())
10572            }
10573            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10574            // single-owner, so there is still nothing to record.
10575            //
10576            // v7.39 (round 652) — but the name now reaches the engine,
10577            // which refuses a role that does not exist as PG does. The
10578            // no-op was swallowing the whole statement, so a dump naming
10579            // a role this server never heard of restored clean and left
10580            // the table owned by whoever ran the restore.
10581            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10582                self.advance();
10583                if matches!(self.peek(), Token::To) {
10584                    self.advance();
10585                }
10586                let role = self.expect_ident_like()?;
10587                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10588                    role
10589                }])
10590            }
10591            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10592            // PG sets a hint; SPG doesn't have clustered storage, so the
10593            // hint itself stays a no-op.
10594            //
10595            // v7.39 (round 652) — the index name is checked now. PG
10596            // errors on one that does not exist, and swallowing that let
10597            // a typo'd CLUSTER ON pass silently.
10598            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10599                self.advance();
10600                // `ON` is a reserved token, not an ident.
10601                if !matches!(self.peek(), Token::On) {
10602                    return Err(self.err(alloc::format!(
10603                        "expected ON after CLUSTER, got {:?}",
10604                        self.peek()
10605                    )));
10606                }
10607                self.advance();
10608                let index = self.expect_ident_like()?;
10609                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10610                    index: Some(index)
10611                }])
10612            }
10613            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10614            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10615            // what a logical decoder puts in the old-tuple image; SPG's
10616            // replication is SQL-text, so there is nothing to record.
10617            // Accept-and-no-op (it used to be a parse error).
10618            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10619                self.advance();
10620                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10621                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10622                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10623                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10624                {
10625                    self.advance(); // IDENTITY
10626                    self.advance(); // USING
10627                    if matches!(self.peek(), Token::Index)
10628                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10629                    {
10630                        self.advance();
10631                    }
10632                    let index = self.expect_ident_like()?;
10633                    self.consume_until_statement_boundary();
10634                    return Ok(alloc::vec![
10635                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10636                    ]);
10637                }
10638                self.consume_until_statement_boundary();
10639                Ok(Vec::new())
10640            }
10641            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10642            //
10643            // v7.39 (round 652) — it used to consume the statement and
10644            // return nothing, on the stated theory that SPG validated at
10645            // ADD CONSTRAINT time so there was never anything left to
10646            // validate. Measured against PG18, ADD CONSTRAINT did not
10647            // scan the existing rows at all — the comment described a
10648            // property SPG did not have, which is why nobody looked. Both
10649            // halves are real now: ADD scans unless told NOT VALID, and
10650            // this scans what NOT VALID skipped.
10651            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10652                self.advance();
10653                self.expect_keyword_ident("constraint")?;
10654                let name = self.expect_ident_like()?;
10655                Ok(alloc::vec![
10656                    crate::ast::AlterTableTarget::ValidateConstraint { name }
10657                ])
10658            }
10659            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10660            // SET (option = value, …). PG uses it to clear per-table
10661            // storage params like fillfactor or autovacuum_*. SPG
10662            // engine-manages those parameters; accept-and-no-op.
10663            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10664                self.advance();
10665                self.consume_until_statement_boundary();
10666                Ok(Vec::new())
10667            }
10668            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10669            // type-of binding (PG 9.0+). SPG composite types
10670            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10671            // TABLE OF is rare and inverse of CREATE TABLE OF.
10672            // Accept-and-no-op until a customer dump round-trips it.
10673            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10674                self.advance();
10675                // v7.39 (round 710) — the type name is validated now.
10676                let type_name = self.expect_ident_like()?;
10677                self.consume_until_statement_boundary();
10678                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10679                    type_name
10680                }])
10681            }
10682            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10683            // (reserved keyword) rather than Token::Ident("not"),
10684            // so it needs its own arm. Accept-and-no-op same as OF.
10685            Token::Not => {
10686                self.advance();
10687                self.consume_until_statement_boundary();
10688                Ok(Vec::new())
10689            }
10690            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10691            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10692                self.advance();
10693                self.expect_row_level_security()?;
10694                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10695                    enabled: None,
10696                    force: Some(true),
10697                }])
10698            }
10699            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10700            Token::Ident(s)
10701                if s.eq_ignore_ascii_case("no")
10702                    && matches!(
10703                        self.tokens.get(self.pos + 1),
10704                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10705                    ) =>
10706            {
10707                self.advance(); // NO
10708                self.advance(); // FORCE
10709                self.expect_row_level_security()?;
10710                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10711                    enabled: None,
10712                    force: Some(false),
10713                }])
10714            }
10715            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10716            // (sets relrowsecurity). The guard requires the next token to be
10717            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10718            Token::Ident(s)
10719                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10720                    && matches!(
10721                        self.tokens.get(self.pos + 1),
10722                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10723                    ) =>
10724            {
10725                let enabled = s.eq_ignore_ascii_case("enable");
10726                self.advance(); // ENABLE/DISABLE
10727                self.expect_row_level_security()?;
10728                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10729                    enabled: Some(enabled),
10730                    force: None,
10731                }])
10732            }
10733            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10734                self.advance();
10735                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10736                // {INDEX|KEY} [name] (cols)`, which every ORM migration
10737                // emits. The same grammar CREATE TABLE already accepts
10738                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
10739                // through the SAME parser — an ALTER-only copy would be a
10740                // second place for the two to drift.
10741                if self.peek_mysql_inline_key_start() {
10742                    return Ok(match self.parse_mysql_inline_key()? {
10743                        Some(c) => {
10744                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
10745                        }
10746                        // FULLTEXT / SPATIAL parse and are accepted as a
10747                        // no-op here exactly as they are inline.
10748                        None => Vec::new(),
10749                    });
10750                }
10751                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
10752                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
10753                // PRIMARY KEY this way; mysqldump emits both.
10754                // Peek-only dispatch (no advance) — `advance()`
10755                // destructively replaces consumed tokens with Eof,
10756                // so saved-pos restore would land on Eofs.
10757                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
10758                {
10759                    // The next-but-one ident is the constraint
10760                    // name; the one after THAT is the kind.
10761                    let kind_pos = self.pos + 2;
10762                    let kind = self.tokens.get(kind_pos).cloned();
10763                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
10764                    {
10765                        let fk = self.parse_table_level_fk()?;
10766                        return Ok(alloc::vec![
10767                            crate::ast::AlterTableTarget::AddForeignKey(fk)
10768                        ]);
10769                    }
10770                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
10771                    {
10772                        self.advance(); // CONSTRAINT
10773                        // v7.39 (read01 round 48) — keep the name; the engine
10774                        // stores it now instead of dropping it on the floor.
10775                        let con_name = self.expect_ident_like()?;
10776                        self.advance(); // PRIMARY
10777                        self.expect_keyword_ident("key")?;
10778                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10779                        // v7.39 (round 711) — the ALTER form carries the
10780                        // timing too (pg_dump writes it here).
10781                        let (deferrable, initially_deferred) =
10782                            self.consume_deferrable_clauses_timed()?;
10783                        return Ok(alloc::vec![
10784                            crate::ast::AlterTableTarget::AddTableConstraint(
10785                                crate::ast::TableConstraint::PrimaryKey {
10786                                    name: Some(con_name),
10787                                    columns: cols,
10788                                    deferrable,
10789                                    initially_deferred,
10790                                }
10791                            )
10792                        ]);
10793                    }
10794                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
10795                    {
10796                        self.advance(); // CONSTRAINT
10797                        // v7.39 (read01 round 48) — keep the name.
10798                        let con_name = self.expect_ident_like()?;
10799                        // v7.22 (mailrs round-13 gap 6) — delegate so
10800                        // the optional `NULLS [NOT] DISTINCT` modifier
10801                        // parses here too (pg_dump emits the ALTER
10802                        // form; semantics enforced by the engine
10803                        // since v7.13).
10804                        let mut uc = self.parse_table_level_unique()?;
10805                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
10806                            *name = Some(con_name);
10807                        }
10808                        return Ok(alloc::vec![
10809                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
10810                        ]);
10811                    }
10812                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
10813                    {
10814                        self.advance(); // CONSTRAINT
10815                        // v7.39 (read01 round 48) — keep the name.
10816                        let con_name = self.expect_ident_like()?;
10817                        self.advance(); // CHECK
10818                        if !matches!(self.peek(), Token::LParen) {
10819                            return Err(self.err(alloc::format!(
10820                                "expected '(' after CHECK, got {:?}", self.peek()
10821                            )));
10822                        }
10823                        self.advance();
10824                        let expr = self.parse_expr(0)?;
10825                        if matches!(self.peek(), Token::RParen) {
10826                            self.advance();
10827                        }
10828                        let not_valid = self.parse_not_valid_suffix();
10829                        return Ok(alloc::vec![
10830                            crate::ast::AlterTableTarget::AddTableConstraint(
10831                                crate::ast::TableConstraint::Check {
10832                                    name: Some(con_name),
10833                                    expr,
10834                                    not_valid,
10835                                }
10836                            )
10837                        ]);
10838                    }
10839                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
10840                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
10841                    // exclusion constraints via this ALTER form.
10842                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
10843                    {
10844                        self.advance(); // CONSTRAINT
10845                        let con_name = self.expect_ident_like()?;
10846                        let mut ex = self.parse_table_level_exclude()?;
10847                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
10848                            *name = Some(con_name);
10849                        }
10850                        return Ok(alloc::vec![
10851                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
10852                        ]);
10853                    }
10854                    // Unknown kind — fall through to FK path which
10855                    // produces a descriptive parse error.
10856                }
10857                let is_fk = matches!(
10858                    self.peek(),
10859                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
10860                        || s.eq_ignore_ascii_case("foreign")
10861                );
10862                if is_fk {
10863                    let fk = self.parse_table_level_fk()?;
10864                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
10865                }
10866                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
10867                // (no CONSTRAINT prefix) — same dispatch.
10868                match self.peek().clone() {
10869                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
10870                        self.advance();
10871                        self.expect_keyword_ident("key")?;
10872                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10873                        let (deferrable, initially_deferred) =
10874                            self.consume_deferrable_clauses_timed()?;
10875                        return Ok(alloc::vec![
10876                            crate::ast::AlterTableTarget::AddTableConstraint(
10877                                crate::ast::TableConstraint::PrimaryKey {
10878                                    name: None,
10879                                    columns: cols,
10880                                    deferrable,
10881                                    initially_deferred,
10882                                }
10883                            )
10884                        ]);
10885                    }
10886                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
10887                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
10888                        let uc = self.parse_table_level_unique()?;
10889                        return Ok(alloc::vec![
10890                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
10891                        ]);
10892                    }
10893                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
10894                    // prefix). The other three bare forms were here and
10895                    // this one was not, so it fell through to the column
10896                    // path and came back as "unexpected reserved keyword
10897                    // 'check' at start of column definition".
10898                    _ if self.peek_table_level_check_start() => {
10899                        let chk = self.parse_table_level_check()?;
10900                        let not_valid = self.parse_not_valid_suffix();
10901                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
10902                            unreachable!("parse_table_level_check returns Check")
10903                        };
10904                        return Ok(alloc::vec![
10905                            crate::ast::AlterTableTarget::AddTableConstraint(
10906                                crate::ast::TableConstraint::Check {
10907                                    name: None,
10908                                    expr,
10909                                    not_valid,
10910                                }
10911                            )
10912                        ]);
10913                    }
10914                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
10915                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
10916                        let ex = self.parse_table_level_exclude()?;
10917                        return Ok(alloc::vec![
10918                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
10919                        ]);
10920                    }
10921                    _ => {}
10922                }
10923                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
10924                    self.advance();
10925                }
10926                let mut if_not_exists = false;
10927                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10928                    self.advance();
10929                    if !matches!(self.peek(), Token::Not) {
10930                        return Err(self.err(alloc::format!(
10931                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
10932                            self.peek()
10933                        )));
10934                    }
10935                    self.advance();
10936                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
10937                        return Err(self.err(alloc::format!(
10938                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
10939                            self.peek()
10940                        )));
10941                    }
10942                    self.advance();
10943                    if_not_exists = true;
10944                }
10945                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
10946                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
10947                // returns ColumnDef + an optional inline FK.
10948                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
10949                let col_name = column.name.clone();
10950                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
10951                    column,
10952                    if_not_exists,
10953                }];
10954                if let Some(mut fk) = col_level_fk {
10955                    if fk.columns.is_empty() {
10956                        fk.columns.push(col_name);
10957                    }
10958                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
10959                }
10960                Ok(out)
10961            }
10962            Token::Drop => {
10963                self.advance();
10964                // v7.13.3 — dispatch on the next token. mailrs round-7
10965                // S8 closed DROP COLUMN; round-6 S7 closed
10966                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
10967                // RESTRICT modifiers.
10968                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
10969                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
10970                let subject = match self.peek() {
10971                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
10972                        self.advance();
10973                        "constraint"
10974                    }
10975                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
10976                        self.advance();
10977                        "column"
10978                    }
10979                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
10980                    // `INDEX` lexes as the reserved Token::Index, so it is
10981                    // unambiguous. `KEY` is a plain ident, and PG allows a
10982                    // column literally named "key", so only read it as the
10983                    // keyword when a name follows it.
10984                    Token::Index => {
10985                        self.advance();
10986                        "index"
10987                    }
10988                    Token::Ident(s)
10989                        if s.eq_ignore_ascii_case("key")
10990                            && matches!(
10991                                self.tokens.get(self.pos + 1),
10992                                Some(Token::Ident(_) | Token::QuotedIdent(_))
10993                            ) =>
10994                    {
10995                        self.advance();
10996                        "index"
10997                    }
10998                    // PG-canonical bare `DROP <col>` without COLUMN
10999                    // keyword is also valid; treat any other ident
11000                    // as the column name.
11001                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11002                    other => {
11003                        return Err(self.err(alloc::format!(
11004                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11005                        )));
11006                    }
11007                };
11008                let mut if_exists = false;
11009                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11010                    let n1 = self.tokens.get(self.pos + 1);
11011                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11012                        self.advance();
11013                        self.advance();
11014                        if_exists = true;
11015                    }
11016                }
11017                let name = self.expect_ident_like()?;
11018                let mut cascade = false;
11019                if matches!(
11020                    self.peek(),
11021                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11022                        || s.eq_ignore_ascii_case("restrict")
11023                ) {
11024                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11025                    {
11026                        cascade = true;
11027                    }
11028                    self.advance();
11029                }
11030                if subject == "index" {
11031                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11032                        name,
11033                        if_exists,
11034                    }])
11035                } else if subject == "constraint" {
11036                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11037                        name,
11038                        if_exists,
11039                    }])
11040                } else {
11041                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11042                        column: name,
11043                        if_exists,
11044                        cascade,
11045                    }])
11046                }
11047            }
11048            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11049                self.advance();
11050                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11051                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11052                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11053                // immediately; accept-and-no-op.
11054                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11055                    self.advance();
11056                    self.consume_until_statement_boundary();
11057                    return Ok(Vec::new());
11058                }
11059                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11060                    self.advance();
11061                }
11062                let col_name = self.expect_ident_like()?;
11063                match self.peek() {
11064                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11065                        self.advance();
11066                    }
11067                    // v7.14.0 — pg_dump emits BIGSERIAL via
11068                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11069                    // nextval('seq')` (the sequence is created
11070                    // separately). SPG's BIGSERIAL already uses
11071                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11072                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11073                    // engine no-ops by consuming the tail.
11074                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11075                        // v7.22 (round-13 T2) — `SET DEFAULT
11076                        // nextval('…')` is how pg_dump spells a
11077                        // SERIAL column (plain integer in CREATE
11078                        // TABLE + this ALTER). It used to be
11079                        // swallowed as a no-op, which silently
11080                        // STRIPPED auto-increment from imported
11081                        // schemas — the first post-import INSERT
11082                        // without an explicit id then violated NOT
11083                        // NULL. Lower it to the auto-increment
11084                        // marker instead.
11085                        let is_default_nextval =
11086                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11087                                && matches!(
11088                                    self.tokens.get(self.pos + 2),
11089                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11090                                );
11091                        if is_default_nextval {
11092                            let seq_name = self.scan_sequence_name_until_boundary();
11093                            return Ok(alloc::vec![
11094                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11095                                    column: col_name,
11096                                    seq_name,
11097                                }
11098                            ]);
11099                        }
11100                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11101                        self.advance(); // consume "set"
11102                        match self.peek().clone() {
11103                            Token::Default => {
11104                                self.advance();
11105                                let default_expr = self.parse_expr(0)?;
11106                                return Ok(alloc::vec![
11107                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11108                                        column: col_name,
11109                                        default_expr,
11110                                    }
11111                                ]);
11112                            }
11113                            Token::Not => {
11114                                self.advance();
11115                                if !matches!(self.peek(), Token::Null) {
11116                                    return Err(self.err(alloc::format!(
11117                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11118                                        self.peek()
11119                                    )));
11120                                }
11121                                self.advance();
11122                                return Ok(alloc::vec![
11123                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11124                                        column: col_name,
11125                                    }
11126                                ]);
11127                            }
11128                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11129                            // stored generated column's expression and
11130                            // recompute existing rows.
11131                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11132                                self.advance(); // EXPRESSION
11133                                if matches!(self.peek(), Token::As) {
11134                                    self.advance();
11135                                }
11136                                let expr = self.parse_expr(0)?;
11137                                return Ok(alloc::vec![
11138                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11139                                        column: col_name,
11140                                        expr,
11141                                    }
11142                                ]);
11143                            }
11144                            other => {
11145                                // Other SET subjects (STATISTICS,
11146                                // STORAGE, COMPRESSION, …) stay no-ops —
11147                                // storage hints with no SPG semantics.
11148                                let _ = other;
11149                                self.consume_until_statement_boundary();
11150                                return Ok(Vec::new());
11151                            }
11152                        }
11153                    }
11154                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11155                        self.advance(); // consume "drop"
11156                        return self.parse_alter_column_drop_tail(col_name);
11157                    }
11158                    Token::Drop => {
11159                        self.advance(); // consume Drop token
11160                        return self.parse_alter_column_drop_tail(col_name);
11161                    }
11162                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11163                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11164                        // GENERATED { ALWAYS | BY DEFAULT } AS
11165                        // IDENTITY ( … )`: pg_dump's spelling for
11166                        // identity columns. Same auto-increment
11167                        // lowering as the nextval default; the
11168                        // sequence options inside the parens are
11169                        // no-ops under SPG's max+1 semantics.
11170                        let is_generated = matches!(
11171                            self.tokens.get(self.pos + 1),
11172                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11173                        );
11174                        if !is_generated {
11175                            return Err(self.err(alloc::format!(
11176                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11177                                self.tokens.get(self.pos + 1)
11178                            )));
11179                        }
11180                        let seq_name = self.scan_sequence_name_until_boundary();
11181                        return Ok(alloc::vec![
11182                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11183                                column: col_name,
11184                                seq_name,
11185                            }
11186                        ]);
11187                    }
11188                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11189                    // column: floor the next allocated value at n (bare
11190                    // RESTART = restart from the start value, 1).
11191                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11192                        self.advance();
11193                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11194                        {
11195                            self.advance();
11196                            let neg = if matches!(self.peek(), Token::Minus) {
11197                                self.advance();
11198                                true
11199                            } else {
11200                                false
11201                            };
11202                            match self.advance() {
11203                                Token::Integer(v) => Some(if neg { -v } else { v }),
11204                                other => {
11205                                    return Err(self.err(alloc::format!(
11206                                        "expected integer after RESTART WITH, got {other:?}"
11207                                    )));
11208                                }
11209                            }
11210                        } else {
11211                            None
11212                        };
11213                        return Ok(alloc::vec![
11214                            crate::ast::AlterTableTarget::AlterColumnRestart {
11215                                column: col_name,
11216                                with,
11217                            }
11218                        ]);
11219                    }
11220                    other => {
11221                        return Err(self.err(alloc::format!(
11222                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11223                        )));
11224                    }
11225                }
11226                // v7.39 (round 713) — the type parser has consumed a
11227                // trailing `COLLATE <name>` since Phase 2.5, and
11228                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11229                // TYPE text COLLATE "C"` parsed clean and changed
11230                // nothing. Keep the clause; the engine re-collates.
11231                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11232                    self.parse_type_with_implied_flags()?;
11233                let collation = if coll_explicit {
11234                    coll_name.map(|n| (coll, n))
11235                } else {
11236                    None
11237                };
11238                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11239                {
11240                    self.advance();
11241                    Some(self.parse_expr(0)?)
11242                } else {
11243                    None
11244                };
11245                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11246                    column: col_name,
11247                    new_type,
11248                    using,
11249                    collation,
11250                }])
11251            }
11252            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11253            // PG also supports `RENAME TO new_table` for table-name
11254            // rename; that surface is deferred (pg_dump never emits
11255            // it). If the first post-RENAME ident is `TO`, the user
11256            // is asking for table rename — error with a clear
11257            // message rather than misparsing `TO` as a column name.
11258            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11259                self.advance();
11260                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11261                // table-name rename (mailrs round-10 A.5 — used
11262                // by migrate-042's `RENAME TO email_contacts`).
11263                // `TO` lexes as Token::To.
11264                if matches!(self.peek(), Token::To)
11265                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11266                {
11267                    self.advance();
11268                    let new = self.expect_ident_like()?;
11269                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11270                        new,
11271                    }]);
11272                }
11273                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11274                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11275                    self.advance();
11276                    let old = self.expect_ident_like()?;
11277                    if matches!(self.peek(), Token::To) {
11278                        self.advance();
11279                    } else {
11280                        self.expect_keyword_ident("to")?;
11281                    }
11282                    let new = self.expect_ident_like()?;
11283                    return Ok(alloc::vec![
11284                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11285                    ]);
11286                }
11287                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11288                    self.advance();
11289                }
11290                let old = self.expect_ident_like()?;
11291                // `TO` is a reserved keyword token; accept both
11292                // Token::To and Token::Ident("to") for consistency.
11293                if matches!(self.peek(), Token::To) {
11294                    self.advance();
11295                } else {
11296                    self.expect_keyword_ident("to")?;
11297                }
11298                let new = self.expect_ident_like()?;
11299                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11300                    old,
11301                    new,
11302                }])
11303            }
11304            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11305            // { ALL | <name> }`. pg_dump --disable-triggers wraps
11306            // every data block with these. Real disable semantics —
11307            // not no-op — because reload correctness assumes the
11308            // triggers don't fire (rows already carry their
11309            // computed values from prod).
11310            Token::Ident(s)
11311                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11312            {
11313                let enabled = s.eq_ignore_ascii_case("enable");
11314                self.advance();
11315                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11316                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11317                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11318                // pg_dump output) — anything else falls through to
11319                // the catch-all error below.
11320                // v7.22 (round-13 T3) — mysqldump wraps every data
11321                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11322                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11323                // maintains indexes incrementally — engine no-op.
11324                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11325                    self.advance();
11326                    return Ok(Vec::new());
11327                }
11328                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11329                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11330                // to gate triggers on session_replication_role; SPG
11331                // has no replica role, so the prefix is consumed and
11332                // treated identically to the plain ENABLE/DISABLE
11333                // TRIGGER form.
11334                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11335                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11336                {
11337                    self.advance();
11338                }
11339                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11340                    return Err(self.err(alloc::format!(
11341                        "expected TRIGGER after {}, got {:?}",
11342                        if enabled { "ENABLE" } else { "DISABLE" },
11343                        self.peek()
11344                    )));
11345                }
11346                self.advance();
11347                // `ALL` lexes as Token::All (reserved); also
11348                // accept Token::Ident("all") for symmetry.
11349                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11350                // TRIGGER selectors. USER (= all user triggers) is
11351                // semantically ALL here; REPLICA / ALWAYS gate on
11352                // session_replication_role which SPG doesn't track.
11353                // All map to TriggerSelector::All.
11354                let which = if matches!(self.peek(), Token::All)
11355                    || matches!(self.peek(), Token::Ident(s)
11356                        if s.eq_ignore_ascii_case("all")
11357                            || s.eq_ignore_ascii_case("user")
11358                            || s.eq_ignore_ascii_case("replica")
11359                            || s.eq_ignore_ascii_case("always"))
11360                {
11361                    self.advance();
11362                    crate::ast::TriggerSelector::All
11363                } else {
11364                    let name = self.expect_ident_like()?;
11365                    crate::ast::TriggerSelector::Named(name)
11366                };
11367                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11368                    which,
11369                    enabled,
11370                }])
11371            }
11372            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11373            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11374                self.advance();
11375                if !matches!(self.peek(), Token::Partition)
11376                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11377                        if s.eq_ignore_ascii_case("partition"))
11378                {
11379                    return Err(self.err(alloc::format!(
11380                        "expected PARTITION after ATTACH, got {:?}",
11381                        self.peek()
11382                    )));
11383                }
11384                self.advance();
11385                let child = self.expect_ident_like()?;
11386                let bounds = self.parse_partition_bounds_tail()?;
11387                Ok(alloc::vec![
11388                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11389                ])
11390            }
11391            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11392            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11393                self.advance();
11394                if !matches!(self.peek(), Token::Partition)
11395                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11396                        if s.eq_ignore_ascii_case("partition"))
11397                {
11398                    return Err(self.err(alloc::format!(
11399                        "expected PARTITION after DETACH, got {:?}",
11400                        self.peek()
11401                    )));
11402                }
11403                self.advance();
11404                let child = self.expect_ident_like()?;
11405                let mut concurrently = false;
11406                let mut finalize = false;
11407                loop {
11408                    match self.peek().clone() {
11409                        Token::Ident(s) | Token::QuotedIdent(s)
11410                            if s.eq_ignore_ascii_case("concurrently") =>
11411                        {
11412                            self.advance();
11413                            concurrently = true;
11414                        }
11415                        Token::Ident(s) | Token::QuotedIdent(s)
11416                            if s.eq_ignore_ascii_case("finalize") =>
11417                        {
11418                            self.advance();
11419                            finalize = true;
11420                        }
11421                        _ => break,
11422                    }
11423                }
11424                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11425                    child,
11426                    concurrently,
11427                    finalize,
11428                }])
11429            }
11430            other => Err(self.err(alloc::format!(
11431                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11432            ))),
11433        }
11434    }
11435
11436    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11437    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11438    /// TABLE … ATTACH PARTITION. Shares the same grammar as
11439    /// `parse_partition_of_tail`'s bounds branch.
11440    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11441    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11442    /// lowering each to the respective AlterTableTarget. Any
11443    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11444    /// no-op via consume_until_statement_boundary.
11445    fn parse_alter_column_drop_tail(
11446        &mut self,
11447        col_name: String,
11448    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11449        match self.peek().clone() {
11450            Token::Default => {
11451                self.advance();
11452                Ok(alloc::vec![
11453                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11454                ])
11455            }
11456            Token::Not => {
11457                self.advance();
11458                if !matches!(self.peek(), Token::Null) {
11459                    return Err(self.err(alloc::format!(
11460                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11461                        self.peek()
11462                    )));
11463                }
11464                self.advance();
11465                Ok(alloc::vec![
11466                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11467                ])
11468            }
11469            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11470            // generated column into a plain column.
11471            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11472                self.advance();
11473                // v7.39 (round 187, U10) — IF EXISTS was consumed but
11474                // dropped, so the engine still errored on a plain
11475                // column; PG's semantics are NOTICE + skip.
11476                let mut if_exists = false;
11477                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11478                    self.advance();
11479                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11480                        self.advance();
11481                        if_exists = true;
11482                    }
11483                }
11484                Ok(alloc::vec![
11485                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
11486                        column: col_name,
11487                        if_exists,
11488                    }
11489                ])
11490            }
11491            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11492            // identity column into a plain column.
11493            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11494                self.advance();
11495                let mut if_exists = false;
11496                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11497                    self.advance();
11498                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11499                        self.advance();
11500                        if_exists = true;
11501                    }
11502                }
11503                Ok(alloc::vec![
11504                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11505                        column: col_name,
11506                        if_exists,
11507                    }
11508                ])
11509            }
11510            _ => {
11511                self.consume_until_statement_boundary();
11512                Ok(Vec::new())
11513            }
11514        }
11515    }
11516
11517    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11518    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11519    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11520    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11521    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11522        let mut opts = crate::ast::CopyOptions::default();
11523        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11524            return Ok(opts);
11525        }
11526        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11527            self.advance();
11528        }
11529        if matches!(self.peek(), Token::LParen) {
11530            self.advance();
11531            loop {
11532                self.parse_one_copy_option(&mut opts)?;
11533                match self.peek() {
11534                    Token::Comma => {
11535                        self.advance();
11536                    }
11537                    Token::RParen => {
11538                        self.advance();
11539                        break;
11540                    }
11541                    other => {
11542                        return Err(self.err(alloc::format!(
11543                            "expected ',' or ')' in COPY options, got {other:?}"
11544                        )));
11545                    }
11546                }
11547            }
11548        } else {
11549            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11550                self.parse_one_copy_option(&mut opts)?;
11551            }
11552        }
11553        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11554            return Err(self.err(alloc::format!(
11555                "unexpected token after COPY options: {:?}",
11556                self.peek()
11557            )));
11558        }
11559        Ok(opts)
11560    }
11561
11562    fn parse_one_copy_option(
11563        &mut self,
11564        opts: &mut crate::ast::CopyOptions,
11565    ) -> Result<(), ParseError> {
11566        use crate::ast::CopyFormat;
11567        // The option keyword. NULL lexes as its own token; the rest are
11568        // bare identifiers.
11569        let kw = match self.advance() {
11570            Token::Null => alloc::string::String::from("NULL"),
11571            Token::Ident(s) => s.to_uppercase(),
11572            other => {
11573                return Err(self.err(alloc::format!(
11574                    "expected a COPY option keyword, got {other:?}"
11575                )));
11576            }
11577        };
11578        match kw.as_str() {
11579            "FORMAT" => {
11580                let fmt = self.expect_ident_like()?;
11581                match fmt.to_ascii_uppercase().as_str() {
11582                    "CSV" => opts.format = CopyFormat::Csv,
11583                    "TEXT" => opts.format = CopyFormat::Text,
11584                    other => {
11585                        return Err(self.err(alloc::format!(
11586                            "COPY format \"{}\" not recognized",
11587                            other.to_ascii_lowercase()
11588                        )));
11589                    }
11590                }
11591            }
11592            // Legacy bare format keywords.
11593            "CSV" => opts.format = CopyFormat::Csv,
11594            "TEXT" => opts.format = CopyFormat::Text,
11595            "HEADER" => {
11596                opts.header = match self.peek() {
11597                    Token::True => {
11598                        self.advance();
11599                        true
11600                    }
11601                    Token::False => {
11602                        self.advance();
11603                        false
11604                    }
11605                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11606                        self.advance();
11607                        true
11608                    }
11609                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11610                        self.advance();
11611                        false
11612                    }
11613                    // Bare HEADER (no boolean) means HEADER true.
11614                    _ => true,
11615                };
11616            }
11617            "DELIMITER" | "QUOTE" | "ESCAPE" => {
11618                let s = match self.advance() {
11619                    Token::String(s) => s,
11620                    other => {
11621                        return Err(self.err(alloc::format!(
11622                            "COPY {kw} expects a single-character string, got {other:?}"
11623                        )));
11624                    }
11625                };
11626                // v7.39 (round 247) — PG's wording (0A000), keyword in
11627                // lowercase: "COPY delimiter must be a single one-byte
11628                // character".
11629                let one_byte_err = || {
11630                    self.err(alloc::format!(
11631                        "COPY {} must be a single one-byte character",
11632                        kw.to_ascii_lowercase()
11633                    ))
11634                };
11635                let mut chars = s.chars();
11636                let c = chars.next().ok_or_else(one_byte_err)?;
11637                if chars.next().is_some() || c.len_utf8() != 1 {
11638                    return Err(one_byte_err());
11639                }
11640                match kw.as_str() {
11641                    "DELIMITER" => opts.delimiter = Some(c),
11642                    "QUOTE" => opts.quote = Some(c),
11643                    _ => opts.escape = Some(c),
11644                }
11645            }
11646            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11647            "FORCE_QUOTE" => {
11648                if matches!(self.peek(), Token::Star) {
11649                    self.advance();
11650                    opts.force_quote = Some(Vec::new());
11651                } else {
11652                    if !matches!(self.peek(), Token::LParen) {
11653                        return Err(self.err(alloc::format!(
11654                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11655                            self.peek()
11656                        )));
11657                    }
11658                    self.advance();
11659                    let mut cols = Vec::new();
11660                    loop {
11661                        cols.push(self.expect_ident_like()?);
11662                        match self.peek() {
11663                            Token::Comma => {
11664                                self.advance();
11665                            }
11666                            Token::RParen => {
11667                                self.advance();
11668                                break;
11669                            }
11670                            other => {
11671                                return Err(self.err(alloc::format!(
11672                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11673                                )));
11674                            }
11675                        }
11676                    }
11677                    opts.force_quote = Some(cols);
11678                }
11679            }
11680            "NULL" => {
11681                opts.null_str = Some(match self.advance() {
11682                    Token::String(s) => s,
11683                    other => {
11684                        return Err(self.err(alloc::format!(
11685                            "COPY NULL expects a quoted string, got {other:?}"
11686                        )));
11687                    }
11688                });
11689            }
11690            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11691            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11692            // FORCE_NULL too.
11693            "FORCE_NOT_NULL" | "FORCE_NULL" => {
11694                let cols = self.parse_copy_column_list(&kw)?;
11695                if kw == "FORCE_NOT_NULL" {
11696                    opts.force_not_null = Some(cols);
11697                } else {
11698                    opts.force_null = Some(cols);
11699                }
11700            }
11701            other => {
11702                // PG's wording, lowercased option name.
11703                return Err(self.err(alloc::format!(
11704                    "option \"{}\" not recognized",
11705                    other.to_ascii_lowercase()
11706                )));
11707            }
11708        }
11709        Ok(())
11710    }
11711
11712    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11713    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11714    /// is the `*` spelling.
11715    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11716        if matches!(self.peek(), Token::Star) {
11717            self.advance();
11718            return Ok(Vec::new());
11719        }
11720        if !matches!(self.peek(), Token::LParen) {
11721            return Err(self.err(alloc::format!(
11722                "expected '(' or '*' after {kw}, got {:?}",
11723                self.peek()
11724            )));
11725        }
11726        self.advance();
11727        let mut cols = Vec::new();
11728        loop {
11729            cols.push(self.expect_ident_like()?);
11730            match self.peek() {
11731                Token::Comma => {
11732                    self.advance();
11733                }
11734                Token::RParen => {
11735                    self.advance();
11736                    break;
11737                }
11738                other => {
11739                    return Err(self.err(alloc::format!(
11740                        "expected ',' or ')' in {kw} list, got {other:?}"
11741                    )));
11742                }
11743            }
11744        }
11745        Ok(cols)
11746    }
11747
11748    fn parse_partition_bounds_tail(
11749        &mut self,
11750    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
11751        use crate::ast::PartitionOfBoundsAst;
11752        match self.peek() {
11753            Token::Default => {
11754                self.advance();
11755                Ok(PartitionOfBoundsAst::Default)
11756            }
11757            Token::For => {
11758                self.advance();
11759                if !matches!(self.peek(), Token::Values) {
11760                    return Err(
11761                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
11762                    );
11763                }
11764                self.advance();
11765                let want_with = matches!(
11766                    self.peek(),
11767                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
11768                );
11769                if want_with {
11770                    self.advance();
11771                    if !matches!(self.peek(), Token::LParen) {
11772                        return Err(self.err(format!(
11773                            "expected '(' after FOR VALUES WITH, got {:?}",
11774                            self.peek()
11775                        )));
11776                    }
11777                    self.advance();
11778                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
11779                    loop {
11780                        let key = self.expect_ident_like()?;
11781                        let n = match self.peek().clone() {
11782                            Token::Integer(v) if u32::try_from(v).is_ok() => {
11783                                self.advance();
11784                                v as u32
11785                            }
11786                            other => {
11787                                return Err(self.err(format!(
11788                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
11789                                )));
11790                            }
11791                        };
11792                        match key.to_ascii_uppercase().as_str() {
11793                            "MODULUS" => modulus = Some(n),
11794                            "REMAINDER" => remainder = Some(n),
11795                            other => {
11796                                return Err(self.err(format!(
11797                                    "FOR VALUES WITH: unknown key {other:?}; \
11798                                     expected MODULUS or REMAINDER"
11799                                )));
11800                            }
11801                        }
11802                        match self.peek() {
11803                            Token::Comma => {
11804                                self.advance();
11805                            }
11806                            Token::RParen => {
11807                                self.advance();
11808                                break;
11809                            }
11810                            other => {
11811                                return Err(self.err(format!(
11812                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
11813                                )));
11814                            }
11815                        }
11816                    }
11817                    let modulus = modulus
11818                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
11819                    let remainder = remainder.ok_or_else(|| {
11820                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
11821                    })?;
11822                    if modulus == 0 {
11823                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
11824                    }
11825                    if remainder >= modulus {
11826                        return Err(self.err(format!(
11827                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
11828                        )));
11829                    }
11830                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
11831                }
11832                match self.peek() {
11833                    Token::From => {
11834                        self.advance();
11835                        let lower = Box::new(self.parse_partition_bound_expr()?);
11836                        if !matches!(self.peek(), Token::To) {
11837                            return Err(self.err(format!(
11838                                "expected TO after FROM (...), got {:?}",
11839                                self.peek()
11840                            )));
11841                        }
11842                        self.advance();
11843                        let upper = Box::new(self.parse_partition_bound_expr()?);
11844                        Ok(PartitionOfBoundsAst::Range { lower, upper })
11845                    }
11846                    Token::In => {
11847                        self.advance();
11848                        if !matches!(self.peek(), Token::LParen) {
11849                            return Err(self.err(format!(
11850                                "expected '(' after FOR VALUES IN, got {:?}",
11851                                self.peek()
11852                            )));
11853                        }
11854                        self.advance();
11855                        let mut values = Vec::new();
11856                        loop {
11857                            values.push(self.parse_expr(0)?);
11858                            match self.peek() {
11859                                Token::Comma => {
11860                                    self.advance();
11861                                }
11862                                Token::RParen => {
11863                                    self.advance();
11864                                    break;
11865                                }
11866                                other => {
11867                                    return Err(self.err(format!(
11868                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
11869                                    )));
11870                                }
11871                            }
11872                        }
11873                        if values.is_empty() {
11874                            return Err(
11875                                self.err("FOR VALUES IN requires at least one literal".to_string())
11876                            );
11877                        }
11878                        Ok(PartitionOfBoundsAst::List { values })
11879                    }
11880                    other => Err(self.err(format!(
11881                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
11882                    ))),
11883                }
11884            }
11885            other => Err(self.err(format!(
11886                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
11887            ))),
11888        }
11889    }
11890
11891    /// v7.16.2 — peek for `information_schema.<tbl>` /
11892    /// `pg_catalog.<tbl>` triples and, if matched, consume all
11893    /// three tokens + return a synthetic table name the engine's
11894    /// SELECT path recognises as a virtual view. Returns `None`
11895    /// when the head doesn't look like a meta-qualified name.
11896    /// Used by `parse_table_ref` to bypass the
11897    /// `expect_ident_like` schema-strip for these specific PG
11898    /// meta schemas (mailrs round-10 A.3).
11899    fn try_peek_meta_qualified(&mut self) -> Option<String> {
11900        // Extract the schema name. Must be a plain ident token.
11901        let schema = match self.tokens.get(self.pos) {
11902            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
11903            _ => return None,
11904        };
11905        // Dot.
11906        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
11907            return None;
11908        }
11909        // The table-side ident may lex as a reserved keyword
11910        // (e.g. `Token::Tables`). Tolerate the common ones via a
11911        // helper that reads the trailing token's underlying name.
11912        let tbl = match self.tokens.get(self.pos + 2)? {
11913            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
11914            Token::Tables => "tables".to_string(),
11915            // Other PG meta table names that may collide with
11916            // reserved keywords land here as needed.
11917            _ => return None,
11918        };
11919        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
11920        // names so the synthetic name doesn't double-prefix
11921        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
11922        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
11923            ("__spg_info_", tbl.to_ascii_lowercase())
11924        } else if schema.eq_ignore_ascii_case("pg_catalog") {
11925            // v7.39 (round 541) — only the catalogs SPG actually
11926            // synthesises are rewritten, which is what the BARE path
11927            // has always checked. Anything else keeps its own name and
11928            // takes the ordinary route: `pg_stat_activity` and friends
11929            // resolve through meta_view_result, and a name that is no
11930            // catalog at all gets PG's "relation does not exist"
11931            // instead of a message about a view SPG cannot materialise.
11932            let lowered = tbl.to_ascii_lowercase();
11933            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
11934                self.advance(); // schema
11935                self.advance(); // dot
11936                self.advance(); // tbl
11937                return Some(lowered);
11938            }
11939            let bare = lowered
11940                .strip_prefix("pg_")
11941                .map(alloc::string::String::from)
11942                .unwrap_or(lowered);
11943            ("__spg_pg_", bare)
11944        } else if schema.eq_ignore_ascii_case("mysql") {
11945            // v7.17.0 Phase 3.P0-65 — MySQL system schema
11946            // (`mysql.user`, `mysql.db`). Same synthetic-name
11947            // shape as pg_catalog.
11948            ("__spg_mysql_", tbl.to_ascii_lowercase())
11949        } else {
11950            return None;
11951        };
11952        self.advance(); // schema
11953        self.advance(); // dot
11954        self.advance(); // tbl
11955        Some(alloc::format!("{prefix}{normalised}"))
11956    }
11957
11958    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
11959    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
11960    /// implicit front of every search_path, so a bare reference to a
11961    /// known catalog table always means the catalog table. Only the
11962    /// names the engine actually synthesises are recognised — any
11963    /// other `pg_*` ident stays a user table (mailrs embed round-12).
11964    fn try_peek_meta_bare(&mut self) -> Option<String> {
11965        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
11966        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
11967        // `pg_catalog` at the front of every search_path. (pg_stat_activity
11968        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
11969        // through the meta_view_result path instead, and already resolve
11970        // bare — they must NOT be listed here or the __spg_ rewrite would
11971        // mis-target them.)
11972        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
11973        let name = match self.tokens.get(self.pos) {
11974            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
11975            _ => return None,
11976        };
11977        // A following dot means this ident is a schema qualifier,
11978        // not a table name — let the qualified path handle it.
11979        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
11980            return None;
11981        }
11982        if !PG_META_TABLES.contains(&name.as_str()) {
11983            return None;
11984        }
11985        self.advance();
11986        let bare = name.strip_prefix("pg_").unwrap_or(&name);
11987        Some(alloc::format!("__spg_pg_{bare}"))
11988    }
11989
11990    /// Consume a bare ident if its lowercase matches `kw`, else err.
11991    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
11992    /// Peeks only; the caller advances.
11993    fn peek_keyword_ident(&self, kw: &str) -> bool {
11994        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
11995    }
11996
11997    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
11998        match self.advance() {
11999            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12000            other => Err(ParseError {
12001                message: format!("expected {kw:?}, got {other:?}"),
12002                token_pos: self.consumed_pos(),
12003            }),
12004        }
12005    }
12006
12007    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12008    /// literal (`'foo'`) — same shape used by CREATE USER for the
12009    /// username slot.
12010    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12011        match self.advance() {
12012            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12013            other => Err(ParseError {
12014                message: format!("expected identifier or string, got {other:?}"),
12015                token_pos: self.consumed_pos(),
12016            }),
12017        }
12018    }
12019
12020    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12021        match self.advance() {
12022            Token::String(s) => Ok(s),
12023            other => Err(ParseError {
12024                message: format!("expected quoted string, got {other:?}"),
12025                token_pos: self.consumed_pos(),
12026            }),
12027        }
12028    }
12029
12030    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12031        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12032        // subqueries recurse through here without passing
12033        // parse_expr; share the same nesting budget.
12034        self.enter_nested()?;
12035        let r = self.parse_select_stmt_inner();
12036        self.nest_depth -= 1;
12037        r
12038    }
12039
12040    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12041        // Caller dispatches on Token::Select; the inner helper handles
12042        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12043        // get a fresh bare-select parse and may not have their own ORDER
12044        // BY / LIMIT.
12045        let mut head = self.parse_bare_select()?;
12046        self.parse_setop_chain_into(&mut head)?;
12047        self.parse_select_tail_into(&mut head)?;
12048        Ok(Statement::Select(head))
12049    }
12050
12051    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12052    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12053    /// token), and INTERSECT [ALL] (a bare ident — it was never
12054    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12055    /// tighter than UNION / EXCEPT — the executor folds the chain
12056    /// left-to-right, which is already correct for LEADING
12057    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12058    /// pair nests into that previous peer, so A UNION B INTERSECT C
12059    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12060    /// groups.
12061    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12062        // A parenthesized group arrives with its own (already
12063        // regrouped) unions on `head`; only the pairs THIS chain
12064        // appends participate in the precedence regroup below —
12065        // nesting an outer INTERSECT into a group-internal peer
12066        // would dissolve the explicit grouping.
12067        let boundary = head.unions.len();
12068        loop {
12069            let base = match self.peek() {
12070                Token::Union => UnionKind::Distinct,
12071                Token::Except => UnionKind::Except,
12072                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12073                _ => break,
12074            };
12075            self.advance();
12076            let kind = if matches!(self.peek(), Token::All) {
12077                self.advance();
12078                match base {
12079                    UnionKind::Distinct => UnionKind::All,
12080                    UnionKind::Except => UnionKind::ExceptAll,
12081                    _ => UnionKind::IntersectAll,
12082                }
12083            } else {
12084                base
12085            };
12086            let peer = self.parse_bare_select()?;
12087            head.unions.push((kind, peer));
12088        }
12089        let mut pairs = core::mem::take(&mut head.unions);
12090        let tail = pairs.split_off(boundary);
12091        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12092        for (kind, peer) in tail {
12093            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12094            // An intersect nests into the previous element of THIS
12095            // chain only; with no new previous element it stays at
12096            // the outer level (the left fold applies it to the
12097            // whole head, group included).
12098            match (
12099                is_intersect,
12100                regrouped.len() > boundary,
12101                regrouped.last_mut(),
12102            ) {
12103                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12104                _ => regrouped.push((kind, peer)),
12105            }
12106        }
12107        head.unions = regrouped;
12108        Ok(())
12109    }
12110
12111    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12112    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12113    /// the top-level bare VALUES statement reuses it verbatim.
12114    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12115    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12116    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12117    /// where the grouping-set universe is still in scope.
12118    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12119        if !matches!(self.peek(), Token::Order) {
12120            return Ok(Vec::new());
12121        }
12122        self.advance();
12123        if !self.peek_is_by() {
12124            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12125        }
12126        self.advance();
12127        let mut keys = Vec::new();
12128        loop {
12129            // v7.39 (round 691) — save/restore, the discipline this parser
12130            // already uses around `pending_sample_preds`, so a subquery inside
12131            // a key neither inherits nor leaks the channel.
12132            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12133            let saved_coll = self.order_key_collation.take();
12134            let parsed = self.parse_expr(0);
12135            self.in_order_by_key = saved_flag;
12136            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12137            let expr = parsed?;
12138            let desc = if matches!(self.peek(), Token::Desc) {
12139                self.advance();
12140                true
12141            } else if matches!(self.peek(), Token::Asc) {
12142                self.advance();
12143                false
12144            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12145                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12146                // one ordering per type, so the btree comparison operators map
12147                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12148                // would need a custom operator class — honest error.
12149                self.advance();
12150                match self.advance() {
12151                    Token::Lt | Token::LtEq => false,
12152                    Token::Gt | Token::GtEq => true,
12153                    other => {
12154                        return Err(self.err(alloc::format!(
12155                            "ORDER BY USING supports the btree comparison \
12156                             operators (< <= > >=); got {other:?}"
12157                        )));
12158                    }
12159                }
12160            } else {
12161                false
12162            };
12163            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12164            let nulls_first = self.parse_optional_nulls_placement()?;
12165            keys.push(OrderBy {
12166                expr,
12167                desc,
12168                nulls_first,
12169                collation,
12170            });
12171            if matches!(self.peek(), Token::Comma) {
12172                self.advance();
12173            } else {
12174                break;
12175            }
12176        }
12177        Ok(keys)
12178    }
12179
12180    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12181        // v7.39 (round 135) — a grouping-set query may have already parsed +
12182        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12183        // no ORDER BY token is present, keep that pre-set order_by rather than
12184        // clobbering it with an empty list.
12185        let parsed_keys = self.parse_order_by_keys()?;
12186        head.order_by = if parsed_keys.is_empty() {
12187            core::mem::take(&mut head.order_by)
12188        } else {
12189            parsed_keys
12190        };
12191        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12192        // order. PG's grammar takes a limit clause and an offset clause
12193        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12194        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12195        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12196        // spelling died on `expected end of input, got Limit`.
12197        //
12198        // Each may appear at most once, and LIMIT and FETCH FIRST are
12199        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12200        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12201        // A second one is left unconsumed here, which the caller reports
12202        // as trailing input rather than silently taking the last.
12203        let mut saw_limit = false;
12204        let mut saw_offset = false;
12205        loop {
12206            if !saw_limit && matches!(self.peek(), Token::Limit) {
12207                self.advance();
12208                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12209                // PG synonyms for "no limit". Treat both as None
12210                // (no head.limit set) so the engine's existing
12211                // unlimited-result path takes over. Reject was the
12212                // pre-5.1 behaviour and broke pg_dump-flavoured
12213                // tooling that occasionally emits LIMIT NULL.
12214                if self.consume_limit_unbounded_sentinel() {
12215                    head.limit = None;
12216                } else {
12217                    let first = self.parse_limit_expr("LIMIT")?;
12218                    // MySQL `LIMIT offset, count` — the first number is
12219                    // the offset when a comma follows.
12220                    if matches!(self.peek(), Token::Comma) {
12221                        self.advance();
12222                        let count = self.parse_limit_expr("LIMIT")?;
12223                        head.offset = Some(first);
12224                        saw_offset = true;
12225                        head.limit = Some(count);
12226                    } else {
12227                        head.limit = Some(first);
12228                    }
12229                }
12230                saw_limit = true;
12231                continue;
12232            }
12233            if !saw_offset && matches!(self.peek(), Token::Offset) {
12234                self.advance();
12235                // PG also accepts an optional `ROW` / `ROWS` trailer
12236                // after the offset value (`OFFSET 10 ROWS`). The
12237                // FETCH-FIRST branch below relies on the same.
12238                let off = self.parse_limit_expr("OFFSET")?;
12239                self.consume_optional_rows_keyword();
12240                head.offset = Some(off);
12241                saw_offset = true;
12242                continue;
12243            }
12244            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12245            // the SQL-standard alias for LIMIT. PG accepts both
12246            // spellings interchangeably; pg_dump emits FETCH FIRST in
12247            // newer versions. We map it onto `head.limit` so the
12248            // engine path is unified.
12249            if !saw_limit
12250                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12251                    if s.eq_ignore_ascii_case("fetch"))
12252            {
12253                self.advance(); // FETCH
12254                // `FIRST` or `NEXT` (both legal per SQL standard).
12255                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12256                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12257                {
12258                    self.advance();
12259                }
12260                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12261                // implicit 1 — but we always consume one if present).
12262                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12263                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12264                {
12265                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12266                    crate::ast::LimitExpr::Literal(1)
12267                } else {
12268                    self.parse_limit_expr("FETCH FIRST")?
12269                };
12270                // Eat `ROW` / `ROWS` if not already consumed above.
12271                self.consume_optional_rows_keyword();
12272                // Optional `ONLY` (the spec form) — or the SQL:2008
12273                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12274                // now honours WITH TIES by extending past the LIMIT
12275                // truncation point through every row that shares the
12276                // last-kept row's ORDER BY key.
12277                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12278                    if s.eq_ignore_ascii_case("only"))
12279                {
12280                    self.advance();
12281                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12282                    if s.eq_ignore_ascii_case("with"))
12283                {
12284                    self.advance(); // WITH
12285                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12286                        if s.eq_ignore_ascii_case("ties"))
12287                    {
12288                        self.advance();
12289                        head.limit_with_ties = true;
12290                    }
12291                }
12292                head.limit = Some(count);
12293                saw_limit = true;
12294                continue;
12295            }
12296            break;
12297        }
12298        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12299        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12300        //       [ OF table_name [, …] ]
12301        //       [ NOWAIT | SKIP LOCKED ]
12302        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12303        // FOR SHARE OF t2`). SPG is a single-writer engine — every
12304        // SELECT already returns a consistent snapshot — so these
12305        // are accept-and-discard: the parser absorbs them so
12306        // mailrs / Rails / Django code paths that emit `SELECT
12307        // … FOR UPDATE` for advisory pessimistic locking load
12308        // without a parser error. The on-disk locking model is
12309        // unchanged; callers that rely on FOR UPDATE for read-
12310        // through-write ordering still get the right answer
12311        // because SPG serialises writes anyway.
12312        head.locking = self
12313            .consume_optional_for_lock_clauses()
12314            .map(alloc::boxed::Box::new);
12315        Ok(())
12316    }
12317
12318    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12319    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12320    /// LOCKED ]` trailers. Each clause is fully accepted and
12321    /// discarded — SPG's single-writer model already satisfies the
12322    /// callers' implicit ordering requirement. Stops at the first
12323    /// token that isn't `FOR`.
12324    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12325        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12326        // not discarded. PG keeps the strongest of several clauses; the
12327        // policy of the last one wins, which is what this loop records.
12328        let mut seen: Option<crate::ast::LockingClause> = None;
12329        while matches!(self.peek(), Token::For) {
12330            // v7.37.14 (A2.5-stub) — record that this query asked
12331            // for a row lock the parser is about to silently
12332            // discard. Operators surface the count via
12333            // `spg_sql::silent_for_update_count()` so they can
12334            // gauge how much of the workload depends on advisory
12335            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12336            // before v7.37.15's per-row tuple locking lands.
12337            crate::record_silent_for_update_clause();
12338            self.advance(); // FOR
12339            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12340            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12341            let mut no_key = false;
12342            let mut key = false;
12343            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12344                if s.eq_ignore_ascii_case("no"))
12345            {
12346                self.advance(); // NO
12347                no_key = true;
12348                // The next ident should be KEY but be generous;
12349                // anything followed by UPDATE/SHARE is accepted.
12350                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12351                    if s.eq_ignore_ascii_case("key"))
12352                {
12353                    self.advance(); // KEY
12354                }
12355            }
12356            // `KEY` prefix (PG `FOR KEY SHARE`).
12357            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12358                if s.eq_ignore_ascii_case("key"))
12359            {
12360                self.advance(); // KEY
12361                key = true;
12362            }
12363            // Lock-strength keyword: UPDATE / SHARE. Required, but
12364            // we're lenient — an unexpected token here just bails
12365            // (we already consumed FOR; caller's downstream
12366            // dispatch will error if anything actually depends on
12367            // the trailing tokens).
12368            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12369                if s.eq_ignore_ascii_case("update"));
12370            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12371                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12372            {
12373                self.advance();
12374                use crate::ast::LockStrength as LS;
12375                let strength = match (is_update, no_key, key) {
12376                    (true, true, _) => LS::NoKeyUpdate,
12377                    (true, _, _) => LS::Update,
12378                    (false, _, true) => LS::KeyShare,
12379                    (false, _, _) => LS::Share,
12380                };
12381                seen = Some(crate::ast::LockingClause {
12382                    strength,
12383                    of_tables: alloc::vec::Vec::new(),
12384                    policy: crate::ast::LockWait::Wait,
12385                });
12386            } else {
12387                // FOR by itself (or `FOR KEY` with nothing after) —
12388                // give up on the lock-clause path. We've already
12389                // advanced past FOR; further attempts to parse
12390                // here would clobber state.
12391                return seen;
12392            }
12393            // Optional `OF tbl[, tbl …]`. mailrs emits this when
12394            // joining and locking only a subset of tables.
12395            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12396                if s.eq_ignore_ascii_case("of"))
12397            {
12398                self.advance(); // OF
12399                #[allow(clippy::while_let_loop)]
12400                loop {
12401                    match self.peek() {
12402                        Token::Ident(_) | Token::QuotedIdent(_) => {
12403                            // v7.39 (round 294) — the name is CAPTURED now: PG
12404                            // validates it against the FROM clause, and an
12405                            // uncaptured list silently means "lock everything".
12406                            let mut nm = match self.advance() {
12407                                Token::Ident(n) | Token::QuotedIdent(n) => n,
12408                                _ => alloc::string::String::new(),
12409                            };
12410                            // Optional schema-qualified `schema.table`.
12411                            if matches!(self.peek(), Token::Dot) {
12412                                self.advance();
12413                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12414                                {
12415                                    self.advance();
12416                                    nm = n;
12417                                }
12418                            }
12419                            if let Some(c) = seen.as_mut() {
12420                                c.of_tables.push(nm);
12421                            }
12422                        }
12423                        _ => break,
12424                    }
12425                    if matches!(self.peek(), Token::Comma) {
12426                        self.advance();
12427                    } else {
12428                        break;
12429                    }
12430                }
12431            }
12432            // Optional `NOWAIT` | `SKIP LOCKED`.
12433            match self.peek().clone() {
12434                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12435                    self.advance();
12436                    if let Some(c) = seen.as_mut() {
12437                        c.policy = crate::ast::LockWait::NoWait;
12438                    }
12439                }
12440                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12441                    self.advance(); // SKIP
12442                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12443                        if s.eq_ignore_ascii_case("locked"))
12444                    {
12445                        self.advance(); // LOCKED
12446                        if let Some(c) = seen.as_mut() {
12447                            c.policy = crate::ast::LockWait::SkipLocked;
12448                        }
12449                    }
12450                }
12451                _ => {}
12452            }
12453            // Loop: PG allows multiple FOR clauses chained.
12454        }
12455        seen
12456    }
12457
12458    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12459    /// Bind value gets resolved during prepared-statement Execute;
12460    /// the Pratt expression parser would over-accept here (e.g.
12461    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12462    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12463    /// sentinel tokens (PG synonyms for "no limit"). Returns true
12464    /// when one was consumed; caller skips the regular
12465    /// limit-value parse and leaves `head.limit` at None.
12466    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12467        if matches!(self.peek(), Token::Null) {
12468            self.advance();
12469            return true;
12470        }
12471        if matches!(self.peek(), Token::All) {
12472            self.advance();
12473            return true;
12474        }
12475        false
12476    }
12477
12478    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12479    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12480    /// SQL-standard shape. No-op when missing.
12481    fn consume_optional_rows_keyword(&mut self) {
12482        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12483            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12484        {
12485            self.advance();
12486        }
12487    }
12488
12489    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12490    ///
12491    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12492    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12493    /// constant, which is why that spelling keeps the token path below.
12494    ///
12495    /// Constants are folded here rather than carried into the tree: the
12496    /// 15+ execution paths that read the row count go through
12497    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12498    /// means "no limit". A clause the engine could not resolve would
12499    /// therefore return the WHOLE table instead of failing. Folding at
12500    /// parse time keeps that impossible; a non-constant clause is still
12501    /// a clean error (recorded residual — closing it wants a resolution
12502    /// pre-pass on the simple-query path, where `substitute_placeholders`
12503    /// does not run).
12504    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12505        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12506        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12507        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12508        // ONLY` both work (its grammar takes a c_expr). Both measured
12509        // against PG 18.4 in round 305.
12510        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12511            return self.parse_limit_constant(label);
12512        }
12513        // One pass, no rewind: `advance()` takes each token by
12514        // `mem::replace`, so a consumed token reads back as Eof and this
12515        // parser cannot backtrack. Everything — bare literal included —
12516        // is therefore folded from the parsed expression rather than
12517        // re-read from the token stream.
12518        let start = self.pos;
12519        let e = self.parse_expr(0)?;
12520        if let crate::ast::Expr::Placeholder(n) = e {
12521            return Ok(crate::ast::LimitExpr::Placeholder(n));
12522        }
12523        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12524        match fold_limit_constant(&e) {
12525            Some(Ok(v)) if v < 0 => Err(ParseError {
12526                message: alloc::format!("{neg_label} must not be negative"),
12527                token_pos: start,
12528            }),
12529            Some(Ok(v)) => u32::try_from(v)
12530                .map(crate::ast::LimitExpr::Literal)
12531                .map_err(|_| ParseError {
12532                    message: alloc::format!("{label} value too large: {v}"),
12533                    token_pos: start,
12534                }),
12535            Some(Err(message)) => Err(ParseError {
12536                message: message.replace("{L}", neg_label),
12537                token_pos: start,
12538            }),
12539            // v7.39 (round 305, V23) — not foldable at parse time
12540            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12541            // expression; the engine evaluates it once before dispatch.
12542            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12543        }
12544    }
12545
12546    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12547        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12548        // coercion rules, not just an integer token: a NUMERIC rounds half
12549        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12550        // refused with PG's wording ("LIMIT must not be negative", 2201W /
12551        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12552        // content, failing as an input-syntax error on the value. General
12553        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12554        // they need an Expr-carrying LimitExpr variant.
12555        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12556        let err_at = |message: alloc::string::String, pos: usize| ParseError {
12557            message,
12558            token_pos: pos,
12559        };
12560        match self.advance() {
12561            Token::Integer(n) if n >= 0 => u32::try_from(n)
12562                .map(crate::ast::LimitExpr::Literal)
12563                .map_err(|_| ParseError {
12564                    message: alloc::format!("{label} value too large: {n}"),
12565                    token_pos: self.consumed_pos(),
12566                }),
12567            Token::Integer(_) => Err(err_at(
12568                alloc::format!("{neg_label} must not be negative"),
12569                self.pos.saturating_sub(1),
12570            )),
12571            Token::Numeric(t) => {
12572                let pos = self.pos.saturating_sub(1);
12573                let v: f64 = t.parse().map_err(|_| {
12574                    err_at(
12575                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12576                        pos,
12577                    )
12578                })?;
12579                if v < 0.0 {
12580                    return Err(err_at(
12581                        alloc::format!("{neg_label} must not be negative"),
12582                        pos,
12583                    ));
12584                }
12585                // Round half away from zero — PG's numeric→bigint cast.
12586                // (no_std: no f64::round; v is non-negative, so truncating
12587                // v + 0.5 is the same thing.)
12588                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12589                let rounded = (v + 0.5) as u64;
12590                u32::try_from(rounded)
12591                    .map(crate::ast::LimitExpr::Literal)
12592                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12593            }
12594            Token::Minus => {
12595                let pos = self.pos.saturating_sub(1);
12596                match self.peek() {
12597                    Token::Integer(_) | Token::Numeric(_) => {
12598                        self.advance();
12599                        Err(err_at(
12600                            alloc::format!("{neg_label} must not be negative"),
12601                            pos,
12602                        ))
12603                    }
12604                    other => Err(err_at(
12605                        alloc::format!(
12606                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12607                        ),
12608                        pos,
12609                    )),
12610                }
12611            }
12612            Token::String(t) => {
12613                let pos = self.pos.saturating_sub(1);
12614                match t.trim().parse::<i64>() {
12615                    Ok(n) if n < 0 => Err(err_at(
12616                        alloc::format!("{neg_label} must not be negative"),
12617                        pos,
12618                    )),
12619                    Ok(n) => u32::try_from(n)
12620                        .map(crate::ast::LimitExpr::Literal)
12621                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12622                    Err(_) => Err(err_at(
12623                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12624                        pos,
12625                    )),
12626                }
12627            }
12628            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12629            other => Err(ParseError {
12630                message: alloc::format!(
12631                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12632                ),
12633                token_pos: self.consumed_pos(),
12634            }),
12635        }
12636    }
12637
12638    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12639    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12640    /// `unions` empty and `order_by` / `limit` `None`; the top-level
12641    /// `parse_select_stmt` is responsible for filling those in.
12642    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12643    /// call in the expression tree to the per-set integer bitmask
12644    /// (PG semantics: one bit per argument, MSB first; 1 = the key
12645    /// is dropped in this grouping set). Runs during the ROLLUP /
12646    /// CUBE / GROUPING SETS expansion, where the set is known.
12647    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12648    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12649    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12650        if let Expr::FunctionCall { name, .. } = expr
12651            && name.eq_ignore_ascii_case("grouping")
12652        {
12653            if !out.iter().any(|e| e == expr) {
12654                out.push(expr.clone());
12655            }
12656            return;
12657        }
12658        match expr {
12659            Expr::Binary { lhs, rhs, .. } => {
12660                Self::collect_grouping_calls(lhs, out);
12661                Self::collect_grouping_calls(rhs, out);
12662            }
12663            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12664                Self::collect_grouping_calls(expr, out)
12665            }
12666            Expr::FunctionCall { args, .. } => {
12667                for a in args {
12668                    Self::collect_grouping_calls(a, out);
12669                }
12670            }
12671            Expr::Case {
12672                operand,
12673                branches,
12674                else_branch,
12675            } => {
12676                if let Some(o) = operand {
12677                    Self::collect_grouping_calls(o, out);
12678                }
12679                for (c, v) in branches {
12680                    Self::collect_grouping_calls(c, out);
12681                    Self::collect_grouping_calls(v, out);
12682                }
12683                if let Some(x) = else_branch {
12684                    Self::collect_grouping_calls(x, out);
12685                }
12686            }
12687            _ => {}
12688        }
12689    }
12690
12691    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12692    /// `grp_exprs[k]` with a reference to the synthetic ordering column
12693    /// `__grp_ord_k` (injected per grouping-set branch).
12694    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12695        if let Expr::FunctionCall { name, .. } = expr
12696            && name.eq_ignore_ascii_case("grouping")
12697        {
12698            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
12699                *expr = Expr::Column(crate::ast::ColumnName {
12700                    qualifier: None,
12701                    name: alloc::format!("__grp_ord_{k}"),
12702                });
12703            }
12704            return;
12705        }
12706        match expr {
12707            Expr::Binary { lhs, rhs, .. } => {
12708                Self::rewrite_grouping_to_col(lhs, grp_exprs);
12709                Self::rewrite_grouping_to_col(rhs, grp_exprs);
12710            }
12711            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12712                Self::rewrite_grouping_to_col(expr, grp_exprs)
12713            }
12714            Expr::FunctionCall { args, .. } => {
12715                for a in args {
12716                    Self::rewrite_grouping_to_col(a, grp_exprs);
12717                }
12718            }
12719            Expr::Case {
12720                operand,
12721                branches,
12722                else_branch,
12723            } => {
12724                if let Some(o) = operand {
12725                    Self::rewrite_grouping_to_col(o, grp_exprs);
12726                }
12727                for (c, v) in branches {
12728                    Self::rewrite_grouping_to_col(c, grp_exprs);
12729                    Self::rewrite_grouping_to_col(v, grp_exprs);
12730                }
12731                if let Some(x) = else_branch {
12732                    Self::rewrite_grouping_to_col(x, grp_exprs);
12733                }
12734            }
12735            _ => {}
12736        }
12737    }
12738
12739    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
12740    /// as the list of key sets it contributes. A bare expression is one
12741    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
12742    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
12743    /// the concatenation of its items' sets, where an item is itself an
12744    /// element, a parenthesized key list, or the empty set `()`. A
12745    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
12746    /// move together.
12747    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
12748        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
12749        // ROLLUP ( … ) / CUBE ( … )
12750        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
12751            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
12752        {
12753            let is_cube = is_kw(self.peek(), "cube");
12754            self.advance(); // ROLLUP / CUBE
12755            self.advance(); // (
12756            let mut units: Vec<Vec<Expr>> = Vec::new();
12757            loop {
12758                if matches!(self.peek(), Token::LParen) {
12759                    // Composite unit: (a, b) rolls up as one.
12760                    self.advance();
12761                    let mut unit = Vec::new();
12762                    if !matches!(self.peek(), Token::RParen) {
12763                        loop {
12764                            unit.push(self.parse_expr(0)?);
12765                            match self.peek() {
12766                                Token::Comma => {
12767                                    self.advance();
12768                                }
12769                                Token::RParen => break,
12770                                other => {
12771                                    return Err(self.err(format!(
12772                                        "expected ',' or ')' in grouping unit, got {other:?}"
12773                                    )));
12774                                }
12775                            }
12776                        }
12777                    }
12778                    self.advance(); // )
12779                    units.push(unit);
12780                } else {
12781                    units.push(alloc::vec![self.parse_expr(0)?]);
12782                }
12783                match self.peek() {
12784                    Token::Comma => {
12785                        self.advance();
12786                    }
12787                    Token::RParen => break,
12788                    other => {
12789                        return Err(self.err(format!(
12790                            "expected ',' or ')' in grouping list, got {other:?}"
12791                        )));
12792                    }
12793                }
12794            }
12795            self.advance(); // )
12796            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
12797                units
12798                    .iter()
12799                    .zip(unit_sel.iter())
12800                    .filter(|(_, keep)| **keep)
12801                    .flat_map(|(u, _)| u.iter().cloned())
12802                    .collect()
12803            };
12804            let n = units.len();
12805            if is_cube {
12806                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
12807                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
12808                    .collect();
12809                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
12810                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
12811            }
12812            return Ok((0..=n)
12813                .rev()
12814                .map(|keep| {
12815                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
12816                    flatten(&sel)
12817                })
12818                .collect());
12819        }
12820        // GROUPING SETS ( item [, item]* )
12821        if is_kw(self.peek(), "grouping")
12822            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
12823        {
12824            self.advance(); // GROUPING
12825            self.advance(); // SETS
12826            if !matches!(self.peek(), Token::LParen) {
12827                return Err(self.err(format!(
12828                    "expected '(' after GROUPING SETS, got {:?}",
12829                    self.peek()
12830                )));
12831            }
12832            self.advance(); // outer (
12833            let mut sets: Vec<Vec<Expr>> = Vec::new();
12834            loop {
12835                if matches!(self.peek(), Token::LParen) {
12836                    // A parenthesized key list (or the empty set).
12837                    self.advance();
12838                    let mut set = Vec::new();
12839                    if !matches!(self.peek(), Token::RParen) {
12840                        loop {
12841                            set.push(self.parse_expr(0)?);
12842                            match self.peek() {
12843                                Token::Comma => {
12844                                    self.advance();
12845                                }
12846                                Token::RParen => break,
12847                                other => {
12848                                    return Err(self.err(format!(
12849                                        "expected ',' or ')' in grouping set, got {other:?}"
12850                                    )));
12851                                }
12852                            }
12853                        }
12854                    }
12855                    self.advance(); // )
12856                    sets.push(set);
12857                } else {
12858                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
12859                    // bare expression.
12860                    sets.extend(self.parse_grouping_element()?);
12861                }
12862                match self.peek() {
12863                    Token::Comma => {
12864                        self.advance();
12865                    }
12866                    Token::RParen => break,
12867                    other => {
12868                        return Err(self.err(format!(
12869                            "expected ',' or ')' after a grouping set, got {other:?}"
12870                        )));
12871                    }
12872                }
12873            }
12874            self.advance(); // outer )
12875            return Ok(sets);
12876        }
12877        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
12878    }
12879
12880    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
12881        // v7.38 (read01) — a reference to a key that is dropped in this grouping
12882        // set evaluates to NULL, at any depth. Previously only a *top-level*
12883        // select item equal to a dropped key was nullified, so a key nested in
12884        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
12885        // column and failed to resolve against the set's synthetic schema.
12886        if dropped.iter().any(|d| d == expr) {
12887            *expr = Expr::Literal(Literal::Null);
12888            return;
12889        }
12890        if let Expr::FunctionCall { name, args } = expr
12891            && name.eq_ignore_ascii_case("grouping")
12892        {
12893            let mut mask: i64 = 0;
12894            for a in args.iter() {
12895                mask <<= 1;
12896                if dropped.iter().any(|d| d == a) {
12897                    mask |= 1;
12898                }
12899            }
12900            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
12901            // literal: a bare integer in a select item is indistinguishable
12902            // from a positional reference once `ORDER BY 1` substitutes the
12903            // item back in, and the round-232 position check then read the
12904            // mask value as an out-of-range position. The cast changes
12905            // nothing semantically (grouping() is integer).
12906            *expr = Expr::Cast {
12907                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
12908                target: crate::ast::CastTarget::Int,
12909            };
12910            return;
12911        }
12912        // Generic recursion over the common expression shapes the
12913        // SELECT list uses; anything without child expressions is
12914        // left alone.
12915        match expr {
12916            Expr::FunctionCall { args, .. } => {
12917                for a in args {
12918                    Self::substitute_grouping_calls(a, dropped);
12919                }
12920            }
12921            Expr::Binary { lhs, rhs, .. } => {
12922                Self::substitute_grouping_calls(lhs, dropped);
12923                Self::substitute_grouping_calls(rhs, dropped);
12924            }
12925            Expr::Unary { expr: inner, .. } => {
12926                Self::substitute_grouping_calls(inner, dropped);
12927            }
12928            Expr::Cast { expr: inner, .. } => {
12929                Self::substitute_grouping_calls(inner, dropped);
12930            }
12931            Expr::Case {
12932                operand,
12933                branches,
12934                else_branch,
12935            } => {
12936                if let Some(op) = operand {
12937                    Self::substitute_grouping_calls(op, dropped);
12938                }
12939                for (w, t) in branches {
12940                    Self::substitute_grouping_calls(w, dropped);
12941                    Self::substitute_grouping_calls(t, dropped);
12942                }
12943                if let Some(e) = else_branch {
12944                    Self::substitute_grouping_calls(e, dropped);
12945                }
12946            }
12947            // v7.38 (read01) — recurse into the remaining child-bearing shapes
12948            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
12949            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
12950            // …` is the canonical rollup-total label idiom).
12951            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
12952            Expr::Like { expr, pattern, .. } => {
12953                Self::substitute_grouping_calls(expr, dropped);
12954                Self::substitute_grouping_calls(pattern, dropped);
12955            }
12956            Expr::InList { expr, list, .. } => {
12957                Self::substitute_grouping_calls(expr, dropped);
12958                for item in list {
12959                    Self::substitute_grouping_calls(item, dropped);
12960                }
12961            }
12962            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
12963            Expr::Array(items) => {
12964                for item in items {
12965                    Self::substitute_grouping_calls(item, dropped);
12966                }
12967            }
12968            Expr::ArraySubscript { target, index } => {
12969                Self::substitute_grouping_calls(target, dropped);
12970                Self::substitute_grouping_calls(index, dropped);
12971            }
12972            Expr::ArraySlice { target, lo, hi } => {
12973                Self::substitute_grouping_calls(target, dropped);
12974                if let Some(lo) = lo {
12975                    Self::substitute_grouping_calls(lo, dropped);
12976                }
12977                if let Some(hi) = hi {
12978                    Self::substitute_grouping_calls(hi, dropped);
12979                }
12980            }
12981            Expr::AnyAll { expr, array, .. } => {
12982                Self::substitute_grouping_calls(expr, dropped);
12983                Self::substitute_grouping_calls(array, dropped);
12984            }
12985            _ => {}
12986        }
12987    }
12988
12989    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
12990        // v7.37.17 (17.6 siblings) — parenthesized set-operation
12991        // group: `( <select chain> )` usable anywhere a query block
12992        // is (head or peer of an outer chain). The group's own
12993        // unions ride the returned SelectStatement; the executor's
12994        // nested-peer recursion runs them.
12995        if matches!(self.peek(), Token::LParen)
12996            && matches!(
12997                self.tokens.get(self.pos + 1),
12998                Some(Token::Select | Token::LParen | Token::Values)
12999            )
13000        {
13001            self.advance(); // (
13002            self.enter_nested()?;
13003            // v7.37 D.20 — a group whose head is a VALUES list:
13004            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13005            // otherwise recurse into a nested SELECT/group head.
13006            let mut head = (if matches!(self.peek(), Token::Values) {
13007                self.advance(); // VALUES
13008                self.parse_values_rows_body()
13009            } else {
13010                self.parse_bare_select()
13011            })
13012            .and_then(|mut h| {
13013                self.parse_setop_chain_into(&mut h)?;
13014                Ok(h)
13015            });
13016            self.nest_depth -= 1;
13017            let mut head = match &mut head {
13018                Ok(h) => core::mem::take(h),
13019                Err(_) => return head,
13020            };
13021            // v7.37.17 (17.6 siblings) — group-internal tail:
13022            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13023            // group head, then wrap the group as a derived table
13024            // (SELECT * FROM (group)) so the outer chain / outer
13025            // tail can't clobber the group's own ordering or limit.
13026            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13027                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13028                    if s.eq_ignore_ascii_case("fetch"));
13029            if has_tail {
13030                self.parse_select_tail_into(&mut head)?;
13031                head = SelectStatement {
13032                    locking: None,
13033                    ctes: Vec::new(),
13034                    distinct: false,
13035                    distinct_on: Vec::new(),
13036                    items: alloc::vec![SelectItem::Wildcard],
13037                    from: Some(FromClause {
13038                        primary: TableRef {
13039                            name: "subquery".to_string(),
13040                            alias: None,
13041                            only: false,
13042                            as_of_segment: None,
13043                            unnest_expr: None,
13044                            unnest_column_aliases: Vec::new(),
13045                            with_ordinality: false,
13046                            generate_series_args: None,
13047                            lateral_subquery: Some(Box::new(head)),
13048                            jsonb_each_text_arg: None,
13049                            table_fn_call: None,
13050                            rows_from: None,
13051                            json_table: None,
13052                            scalar_fn_item: false,
13053                        },
13054                        joins: Vec::new(),
13055                    }),
13056                    where_: None,
13057                    group_by: None,
13058                    group_by_all: false,
13059                    having: None,
13060                    unions: Vec::new(),
13061                    order_by: Vec::new(),
13062                    limit: None,
13063                    offset: None,
13064                    limit_with_ties: false,
13065                    window_check_exprs: Vec::new(),
13066                };
13067            }
13068            if !matches!(self.peek(), Token::RParen) {
13069                return Err(self.err(format!(
13070                    "expected ')' after parenthesized query group, got {:?}",
13071                    self.peek()
13072                )));
13073            }
13074            self.advance();
13075            return Ok(head);
13076        }
13077        // `TABLE name` shorthand as a query block — valid anywhere
13078        // a SELECT head is (set-op peers included).
13079        if matches!(self.peek(), Token::Table)
13080            && matches!(
13081                self.tokens.get(self.pos + 1),
13082                Some(Token::Ident(_) | Token::QuotedIdent(_))
13083            )
13084        {
13085            return self.parse_table_shorthand();
13086        }
13087        if !matches!(self.peek(), Token::Select) {
13088            return Err(self.err(format!(
13089                "expected SELECT to start a query block, got {:?}",
13090                self.peek()
13091            )));
13092        }
13093        self.advance();
13094        let distinct = if matches!(self.peek(), Token::Distinct) {
13095            self.advance();
13096            true
13097        } else {
13098            false
13099        };
13100        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13101        // keep the first row (per ORDER BY) of each group the
13102        // expressions define. Django's .distinct('field') shape.
13103        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13104            self.advance(); // ON
13105            if !matches!(self.peek(), Token::LParen) {
13106                return Err(self.err(format!(
13107                    "expected '(' after DISTINCT ON, got {:?}",
13108                    self.peek()
13109                )));
13110            }
13111            self.advance();
13112            let mut exprs = Vec::new();
13113            loop {
13114                exprs.push(self.parse_expr(0)?);
13115                match self.peek() {
13116                    Token::Comma => {
13117                        self.advance();
13118                    }
13119                    Token::RParen => break,
13120                    other => {
13121                        return Err(self.err(format!(
13122                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13123                        )));
13124                    }
13125                }
13126            }
13127            self.advance(); // )
13128            exprs
13129        } else {
13130            Vec::new()
13131        };
13132        let mut items = self.parse_select_list()?;
13133        // Scope the TABLESAMPLE lowering channel to this SELECT:
13134        // stash whatever an enclosing select accumulated, collect
13135        // our own FROM's predicates, restore after the combine.
13136        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13137        let mut from = if matches!(self.peek(), Token::From) {
13138            self.advance();
13139            Some(self.parse_from_clause()?)
13140        } else {
13141            None
13142        };
13143        // v7.37 D.22 — a set-returning function in the projection with no FROM
13144        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13145        // rows. Move the first SRF projection item to a FROM-position derived
13146        // table and replace it in the projection with a reference to its output
13147        // column; sibling scalar columns repeat per SRF row. PG names the output
13148        // column after the function (or its AS alias). Reuses the FROM-SRF
13149        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13150        // works via the targetlist-SRF path.
13151        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13152        // `SELECT * FROM f(args)` — the record's fields become the columns, which
13153        // is exactly what the function's own row shape already is. Anywhere else
13154        // (per outer row, or beside other items) it would need a real record-typed
13155        // projection, so it says so rather than answering something else.
13156        if let [
13157            SelectItem::Expr {
13158                expr: Expr::FunctionCall { name, args },
13159                ..
13160            },
13161        ] = items.as_slice()
13162            && name == "__record_expand"
13163        {
13164            let Some(Expr::FunctionCall {
13165                name: inner_name,
13166                args: inner_args,
13167            }) = args.first()
13168            else {
13169                return Err(self.err(
13170                    "(<expr>).* expands a function's record — it needs a function call".into(),
13171                ));
13172            };
13173            if from.is_some() {
13174                return Err(self.err(
13175                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13176                        .into(),
13177                ));
13178            }
13179            let fn_ref = TableRef {
13180                name: inner_name.clone(),
13181                alias: None,
13182                only: false,
13183                as_of_segment: None,
13184                unnest_expr: None,
13185                unnest_column_aliases: Vec::new(),
13186                with_ordinality: false,
13187                generate_series_args: None,
13188                lateral_subquery: None,
13189                jsonb_each_text_arg: None,
13190                table_fn_call: Some(Box::new((
13191                    inner_name.to_ascii_lowercase(),
13192                    inner_args.clone(),
13193                ))),
13194                rows_from: None,
13195                json_table: None,
13196                scalar_fn_item: false,
13197            };
13198            items = alloc::vec![SelectItem::Wildcard];
13199            from = Some(FromClause {
13200                primary: fn_ref,
13201                joins: Vec::new(),
13202            });
13203        }
13204        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13205        // FROM, keeps its marker: the ENGINE lowers it, because naming the
13206        // record's fields takes the catalog. It becomes a LATERAL of the same
13207        // function plus one item per declared column — the machinery rounds 65
13208        // and 69 already built.
13209        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13210        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13211        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13212        // express, since the lifted one becomes a scan and the other would
13213        // expand per its rows (a cross product, not a zip). So when the
13214        // projection holds more than one top-level function call, the lift steps
13215        // aside and the engine's target-list expansion takes the whole list.
13216        let fn_call_items = items
13217            .iter()
13218            .filter(|it| {
13219                matches!(
13220                    it,
13221                    SelectItem::Expr {
13222                        expr: Expr::FunctionCall { .. },
13223                        ..
13224                    }
13225                )
13226            })
13227            .count();
13228        if from.is_none() && fn_call_items <= 1 {
13229            let mut found: Option<(usize, TableRef, String)> = None;
13230            for (i, item) in items.iter().enumerate() {
13231                if let SelectItem::Expr {
13232                    expr: Expr::FunctionCall { name, args },
13233                    alias,
13234                } = item
13235                {
13236                    let lname = name.to_ascii_lowercase();
13237                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
13238                    let (unnest, gs) = match lname.as_str() {
13239                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13240                        "generate_series" if (2..=3).contains(&args.len()) => {
13241                            (None, Some(args.clone()))
13242                        }
13243                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
13244                        // no-FROM projection yields the 1-based subscripts, i.e.
13245                        // generate_series(1, array_length(arr, dim)); an invalid
13246                        // dimension makes array_length NULL → 0 rows, as in PG.
13247                        "generate_subscripts" if args.len() == 2 => (
13248                            None,
13249                            Some(alloc::vec![
13250                                Expr::Literal(Literal::Integer(1)),
13251                                Expr::FunctionCall {
13252                                    name: "array_length".to_string(),
13253                                    args: args.clone(),
13254                                },
13255                            ]),
13256                        ),
13257                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13258                        // in a no-FROM projection unnest their *_to_array form.
13259                        "string_to_table" | "regexp_split_to_table" => {
13260                            let array_fn = if lname == "string_to_table" {
13261                                "string_to_array"
13262                            } else {
13263                                "regexp_split_to_array"
13264                            };
13265                            (
13266                                Some(Box::new(Expr::FunctionCall {
13267                                    name: array_fn.to_string(),
13268                                    args: args.clone(),
13269                                })),
13270                                None,
13271                            )
13272                        }
13273                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13274                        // a no-FROM projection expand per element. The scalar form
13275                        // returns the elements as a TEXT array, so unnest over the
13276                        // same call materialises one row each (same rewrite the
13277                        // FROM-clause form uses).
13278                        "jsonb_array_elements"
13279                        | "json_array_elements"
13280                        | "jsonb_array_elements_text"
13281                        | "json_array_elements_text"
13282                            if args.len() == 1 =>
13283                        {
13284                            (
13285                                Some(Box::new(Expr::FunctionCall {
13286                                    name: lname.clone(),
13287                                    args: args.clone(),
13288                                })),
13289                                None,
13290                            )
13291                        }
13292                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13293                        // in a no-FROM projection expands per match (scalar form
13294                        // returns the matches as a TEXT array → unnest).
13295                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13296                            Some(Box::new(Expr::FunctionCall {
13297                                name: lname.clone(),
13298                                args: args.clone(),
13299                            })),
13300                            None,
13301                        ),
13302                        _ => continue,
13303                    };
13304                    found = Some((
13305                        i,
13306                        TableRef {
13307                            name: colname.clone(),
13308                            alias: Some(colname.clone()),
13309                            only: false,
13310                            as_of_segment: None,
13311                            unnest_expr: unnest,
13312                            unnest_column_aliases: alloc::vec![colname.clone()],
13313                            with_ordinality: false,
13314                            generate_series_args: gs,
13315                            lateral_subquery: None,
13316                            jsonb_each_text_arg: None,
13317                            table_fn_call: None,
13318                            rows_from: None,
13319                            json_table: None,
13320                            scalar_fn_item: false,
13321                        },
13322                        colname,
13323                    ));
13324                    break;
13325                }
13326            }
13327            if let Some((idx, tref, colname)) = found {
13328                from = Some(FromClause {
13329                    primary: tref,
13330                    joins: Vec::new(),
13331                });
13332                items[idx] = SelectItem::Expr {
13333                    expr: Expr::Column(ColumnName {
13334                        qualifier: None,
13335                        name: colname.clone(),
13336                    }),
13337                    alias: Some(colname),
13338                };
13339            }
13340        }
13341        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13342        let where_ = if matches!(self.peek(), Token::Where) {
13343            self.advance();
13344            Some(self.parse_expr(0)?)
13345        } else {
13346            None
13347        };
13348        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13349            Some(match acc {
13350                Some(w) => Expr::Binary {
13351                    lhs: Box::new(pred),
13352                    op: crate::ast::BinOp::And,
13353                    rhs: Box::new(w),
13354                },
13355                None => pred,
13356            })
13357        });
13358        self.pending_sample_preds = enclosing_sample_preds;
13359        let mut group_by_all = false;
13360        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13361        // share one expansion: `grouping_sets` lists the key subsets
13362        // (first = primary, assigned to stmt.group_by; the rest
13363        // become UNION ALL peers), `grouping_universe` is the full
13364        // key list used to compute each peer's dropped keys.
13365        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13366        let mut grouping_universe: Vec<Expr> = Vec::new();
13367        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13368        // A BOOL, not the key list: this frame is the statement parser's, and
13369        // round 430 measured that a `Vec` local here is enough on its own to
13370        // tip the 512 KiB nesting guard. The keys are recoverable from
13371        // `grouping_universe`, which a rollup fills with exactly them.
13372        let mut mysql_rollup = false;
13373        let group_by = if matches!(self.peek(), Token::Group) {
13374            self.advance();
13375            if !self.peek_is_by() {
13376                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13377            }
13378            self.advance();
13379            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13380            // every non-aggregate SELECT-list item later.
13381            if matches!(self.peek(), Token::All) {
13382                self.advance();
13383                group_by_all = true;
13384                None
13385            } else {
13386                // v7.39 (round 242) — PG's general grouping-element grammar:
13387                // GROUP BY [DISTINCT] element [, element]*, where an element
13388                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13389                // SETS (…) — mixed freely. Each element yields a list of
13390                // key sets; the query's grouping sets are the CARTESIAN
13391                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13392                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13393                // content. ROLLUP/CUBE members may be composite
13394                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13395                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13396                // parser handled only a lone ROLLUP/CUBE/GS as the whole
13397                // clause.
13398                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13399                    self.advance();
13400                    true
13401                } else {
13402                    false
13403                };
13404                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13405                loop {
13406                    element_sets.push(self.parse_grouping_element()?);
13407                    if matches!(self.peek(), Token::Comma) {
13408                        self.advance();
13409                    } else {
13410                        break;
13411                    }
13412                }
13413                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13414                for el in &element_sets {
13415                    let mut next: Vec<Vec<Expr>> = Vec::new();
13416                    for base in &total {
13417                        for set in el {
13418                            let mut merged = base.clone();
13419                            for k in set {
13420                                if !merged.iter().any(|m| m == k) {
13421                                    merged.push(k.clone());
13422                                }
13423                            }
13424                            next.push(merged);
13425                        }
13426                    }
13427                    total = next;
13428                }
13429                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13430                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13431                // The keys and the aggregates come out identical; the ROW
13432                // ORDER does not, and that is the part a report depends on.
13433                // MySQL interleaves each group's subtotal right after its
13434                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13435                // where the union-of-grouping-sets expansion emits every
13436                // leaf first and then every subtotal. MariaDB REFUSES an
13437                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13438                // order itself — measured on MariaDB 11 and MySQL 9.7, which
13439                // agree on the order and disagree only on whether ORDER BY
13440                // is allowed (MySQL allows it; SPG allows it too, since
13441                // refusing would break the clients that can write it).
13442                if self.mysql_dialect
13443                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13444                    && matches!(
13445                        self.tokens.get(self.pos + 1),
13446                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13447                    )
13448                {
13449                    self.advance(); // WITH
13450                    self.advance(); // ROLLUP
13451                    let keys = total.into_iter().next().unwrap_or_default();
13452                    mysql_rollup = true;
13453                    // n+1 prefixes, largest first — the same expansion
13454                    // `ROLLUP (…)` produces.
13455                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13456                }
13457                if distinct_sets {
13458                    let mut seen: Vec<Vec<String>> = Vec::new();
13459                    total.retain(|set| {
13460                        let mut key: Vec<String> =
13461                            set.iter().map(|e| alloc::format!("{e}")).collect();
13462                        key.sort();
13463                        if seen.contains(&key) {
13464                            false
13465                        } else {
13466                            seen.push(key);
13467                            true
13468                        }
13469                    });
13470                }
13471                if total.len() > 1 {
13472                    let mut universe: Vec<Expr> = Vec::new();
13473                    for set in &total {
13474                        for k in set {
13475                            if !universe.iter().any(|u| u == k) {
13476                                universe.push(k.clone());
13477                            }
13478                        }
13479                    }
13480                    grouping_universe = universe;
13481                    let primary = total[0].clone();
13482                    grouping_sets = total;
13483                    Some(primary)
13484                } else {
13485                    // One set (a plain GROUP BY list, or a single-set
13486                    // spelling like GROUPING SETS ((a, b))). An EMPTY
13487                    // single set — GROUPING SETS (()) — stays
13488                    // `Some(vec![])`: the grand-total group, which must
13489                    // run the aggregate path.
13490                    Some(total.into_iter().next().unwrap_or_default())
13491                }
13492            }
13493        } else {
13494            None
13495        };
13496        let having = if matches!(self.peek(), Token::Having) {
13497            self.advance();
13498            Some(self.parse_expr(0)?)
13499        } else {
13500            None
13501        };
13502        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13503        // OVER w parsed to a marker above; inline each definition
13504        // into the referencing WindowFunction nodes.
13505        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13506        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13507            self.advance();
13508            loop {
13509                let wname = self.expect_ident_like()?;
13510                if !matches!(self.peek(), Token::As) {
13511                    return Err(self.err(format!(
13512                        "expected AS after WINDOW {wname}, got {:?}",
13513                        self.peek()
13514                    )));
13515                }
13516                self.advance();
13517                // v7.39 (round 229) — PG rejects a redefinition outright.
13518                if window_defs
13519                    .iter()
13520                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13521                {
13522                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13523                }
13524                let def = self.parse_over_clause()?;
13525                // A definition may itself copy an earlier one
13526                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13527                // so resolve it against the defs already in scope. Same
13528                // copy rules as an `OVER (w1 …)` in the select list.
13529                let mut probe = Expr::WindowFunction {
13530                    name: String::new(),
13531                    args: Vec::new(),
13532                    partition_by: def.0,
13533                    order_by: def.1,
13534                    frame: def.2,
13535                    null_treatment: crate::ast::NullTreatment::Respect,
13536                    filter: None,
13537                };
13538                Self::substitute_named_windows(&mut probe, &window_defs)
13539                    .map_err(|m| self.err(m))?;
13540                let Expr::WindowFunction {
13541                    partition_by,
13542                    order_by,
13543                    frame,
13544                    ..
13545                } = probe
13546                else {
13547                    unreachable!("probe is a WindowFunction")
13548                };
13549                window_defs.push((wname, (partition_by, order_by, frame)));
13550                if matches!(self.peek(), Token::Comma) {
13551                    self.advance();
13552                    continue;
13553                }
13554                break;
13555            }
13556        }
13557        // v7.39 (round 705) — which definitions did anything reference?
13558        // The ones nothing did used to be dropped here, unexamined, so
13559        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13560        // definition whether referenced or not. Their key expressions ride
13561        // out on the statement for the engine to resolve.
13562        let mut window_refs: Vec<String> = Vec::new();
13563        if !window_defs.is_empty() {
13564            for it in &items {
13565                if let SelectItem::Expr { expr, .. } = it {
13566                    Self::collect_named_window_refs(expr, &mut window_refs);
13567                }
13568            }
13569        }
13570        let window_check_exprs: Vec<Expr> = window_defs
13571            .iter()
13572            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13573            .flat_map(|(_, (partition, order, _))| {
13574                partition
13575                    .iter()
13576                    .cloned()
13577                    .chain(order.iter().map(|(e, _, _)| e.clone()))
13578            })
13579            .collect();
13580        if !window_defs.is_empty()
13581            || items
13582                .iter()
13583                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13584        {
13585            for it in &mut items {
13586                if let SelectItem::Expr { expr, .. } = it {
13587                    Self::substitute_named_windows(expr, &window_defs)
13588                        .map_err(|m| self.err(m))?;
13589                }
13590            }
13591        }
13592        // `GROUP BY 1` — positional keys substitute with the Nth
13593        // select item's expression (same contract ORDER BY has had
13594        // since v6.x). Out-of-range positions error.
13595        let group_by = match group_by {
13596            Some(mut keys) => {
13597                for k in &mut keys {
13598                    if let Expr::Literal(Literal::Integer(n)) = k {
13599                        let idx = *n;
13600                        if idx < 1 || idx as usize > items.len() {
13601                            return Err(self.err(alloc::format!(
13602                                "GROUP BY position {idx} is not in select list"
13603                            )));
13604                        }
13605                        match &items[(idx - 1) as usize] {
13606                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
13607                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13608                                return Err(self.err(alloc::format!(
13609                                    "GROUP BY position {idx} references a wildcard item"
13610                                )));
13611                            }
13612                        }
13613                    }
13614                }
13615                Some(keys)
13616            }
13617            None => None,
13618        };
13619        let mut stmt = SelectStatement {
13620            locking: None,
13621            ctes: Vec::new(),
13622            distinct,
13623            distinct_on,
13624            items,
13625            from,
13626            where_,
13627            group_by,
13628            group_by_all,
13629            having,
13630            unions: Vec::new(),
13631            order_by: Vec::new(),
13632            limit: None,
13633            offset: None,
13634            limit_with_ties: false,
13635            window_check_exprs,
13636        };
13637        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13638        // first set is the primary (already on stmt.group_by); each
13639        // further set becomes a UNION ALL peer with its dropped
13640        // keys (universe minus the set) replaced by NULL literals
13641        // in the peer's items and group_by. PG-legal: non-grouped
13642        // select items must be group keys or aggregates, so a
13643        // dropped key's occurrences in the projection are exactly
13644        // the ones to nullify.
13645        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
13646        // over a plain GROUP BY (every argument must be a group key; the
13647        // mask is then 0) and rejects anything else with 42803. SPG's
13648        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
13649        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
13650        // function `grouping`".
13651        if grouping_sets.len() <= 1 {
13652            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
13653            let mut calls: Vec<Expr> = Vec::new();
13654            for item in &stmt.items {
13655                if let SelectItem::Expr { expr, .. } = item {
13656                    Self::collect_grouping_calls(expr, &mut calls);
13657                }
13658            }
13659            if let Some(h) = &stmt.having {
13660                Self::collect_grouping_calls(h, &mut calls);
13661            }
13662            for call in &calls {
13663                let Expr::FunctionCall { args, .. } = call else {
13664                    continue;
13665                };
13666                for a in args {
13667                    if !keys.iter().any(|k| k == a) {
13668                        return Err(self.err(
13669                            "arguments to GROUPING must be grouping expressions of the associated query level"
13670                                .to_string(),
13671                        ));
13672                    }
13673                }
13674            }
13675            if !calls.is_empty() {
13676                for item in &mut stmt.items {
13677                    if let SelectItem::Expr { expr, .. } = item {
13678                        Self::substitute_grouping_calls(expr, &[]);
13679                    }
13680                }
13681                if let Some(h) = &mut stmt.having {
13682                    Self::substitute_grouping_calls(h, &[]);
13683                }
13684            }
13685        }
13686        if grouping_sets.len() > 1 {
13687            // The primary set's own dropped keys nullify in the
13688            // HEAD's projection too (GROUPING SETS's first set may
13689            // omit keys other sets use).
13690            let primary = grouping_sets[0].clone();
13691            let head_dropped: Vec<Expr> = grouping_universe
13692                .iter()
13693                .filter(|u| !primary.iter().any(|k| k == *u))
13694                .cloned()
13695                .collect();
13696            for set in grouping_sets.iter().skip(1) {
13697                let mut peer = stmt.clone();
13698                peer.unions = Vec::new();
13699                let dropped: Vec<&Expr> = grouping_universe
13700                    .iter()
13701                    .filter(|u| !set.iter().any(|k| k == *u))
13702                    .collect();
13703                // Empty set = grand-total group: `Some(vec![])` forces
13704                // the aggregate path (one collapsed row) instead of a
13705                // per-row passthrough. See the primary-set note above.
13706                peer.group_by = Some(set.clone());
13707                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
13708                for item in &mut peer.items {
13709                    if let SelectItem::Expr { expr, alias } = item {
13710                        if dropped.iter().any(|d| *d == expr) {
13711                            // v7.39 — keep the dropped key's name on the
13712                            // NULL literal so the UNION output column
13713                            // (and any top-level ORDER BY on it) still
13714                            // resolves.
13715                            if alias.is_none()
13716                                && let Expr::Column(c) = &expr
13717                            {
13718                                *alias = Some(c.name.clone());
13719                            }
13720                            *expr = Expr::Literal(Literal::Null);
13721                        } else {
13722                            Self::substitute_grouping_calls(expr, &dropped_owned);
13723                        }
13724                    }
13725                }
13726                if let Some(h) = &mut peer.having {
13727                    Self::substitute_grouping_calls(h, &dropped_owned);
13728                }
13729                stmt.unions.push((UnionKind::All, peer));
13730            }
13731            for item in &mut stmt.items {
13732                if let SelectItem::Expr { expr, alias } = item {
13733                    if head_dropped.iter().any(|d| d == expr) {
13734                        if alias.is_none()
13735                            && let Expr::Column(c) = &expr
13736                        {
13737                            *alias = Some(c.name.clone());
13738                        }
13739                        *expr = Expr::Literal(Literal::Null);
13740                    } else {
13741                        Self::substitute_grouping_calls(expr, &head_dropped);
13742                    }
13743                }
13744            }
13745            if let Some(h) = &mut stmt.having {
13746                Self::substitute_grouping_calls(h, &head_dropped);
13747            }
13748            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
13749            // (while `grouping_universe` / the per-branch sets are in scope). For
13750            // each grouping() call in it, inject a per-branch hidden column
13751            // `__grp_ord_K` carrying that branch's mask into the head + every
13752            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
13753            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
13754            // from the final output. A standalone grouping-set query has ORDER BY
13755            // (not an explicit set-op) next, so consuming it here is safe.
13756            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
13757            // rollup carries the hierarchical order: sort by the grouping
13758            // keys with the rolled-up NULLs last, which is exactly the
13759            // interleaving both oracles emit. A client's own ORDER BY wins,
13760            // which is what MySQL does (MariaDB refuses to let one be
13761            // written at all).
13762            // The synthesised keys have to travel the SAME path a written
13763            // ORDER BY does: the block below is what turns a `grouping()`
13764            // call into the per-branch `__grp_ord_K` column the engine can
13765            // actually sort on. Bypassing it left a bare `grouping(text)`
13766            // for the evaluator to reject.
13767            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
13768                self.parse_order_by_keys()?
13769            } else if mysql_rollup {
13770                Self::mysql_rollup_order(&grouping_universe)
13771            } else {
13772                Vec::new()
13773            };
13774            if !synthesised_or_parsed.is_empty() {
13775                let mut order_keys = synthesised_or_parsed;
13776                let mut grp_exprs: Vec<Expr> = Vec::new();
13777                for ob in &order_keys {
13778                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
13779                }
13780                for (k, gexpr) in grp_exprs.iter().enumerate() {
13781                    let colname = alloc::format!("__grp_ord_{k}");
13782                    // Head branch (primary set) uses `head_dropped`.
13783                    let mut he = gexpr.clone();
13784                    Self::substitute_grouping_calls(&mut he, &head_dropped);
13785                    stmt.items.push(SelectItem::Expr {
13786                        expr: he,
13787                        alias: Some(colname.clone()),
13788                    });
13789                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
13790                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
13791                        let set = &grouping_sets[i + 1];
13792                        let dropped: Vec<Expr> = grouping_universe
13793                            .iter()
13794                            .filter(|u| !set.iter().any(|k| k == *u))
13795                            .cloned()
13796                            .collect();
13797                        let mut pe = gexpr.clone();
13798                        Self::substitute_grouping_calls(&mut pe, &dropped);
13799                        peer.items.push(SelectItem::Expr {
13800                            expr: pe,
13801                            alias: Some(colname.clone()),
13802                        });
13803                    }
13804                }
13805                for ob in &mut order_keys {
13806                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
13807                }
13808                stmt.order_by = order_keys;
13809            }
13810        }
13811        Ok(stmt)
13812    }
13813
13814    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
13815    /// as ORDER BY keys.
13816    ///
13817    /// Per key: the rollup marker, then the key. Sorting on the key alone
13818    /// is not enough, and a table with a NULL in it says why — MariaDB puts
13819    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
13820    /// the ROLLUP-introduced NULL last, and both print as NULL.
13821    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
13822    /// real group including the data-NULL one, 1 only for the row the
13823    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
13824    /// rolls up to NULL|2, a|1, b|3, NULL|6.
13825    ///
13826    /// `#[inline(never)]`: its locals must not join the statement parser's
13827    /// frame, which round 430 measured sitting against the nesting guard.
13828    #[inline(never)]
13829    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
13830        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
13831        for e in keys {
13832            out.push(OrderBy {
13833                expr: Expr::FunctionCall {
13834                    name: "grouping".into(),
13835                    args: alloc::vec![e.clone()],
13836                },
13837                desc: false,
13838                nulls_first: None,
13839                collation: None,
13840            });
13841            out.push(OrderBy {
13842                expr: e.clone(),
13843                desc: false,
13844                // MySQL orders NULL first on an ascending key.
13845                nulls_first: Some(true),
13846                collation: None,
13847            });
13848        }
13849        out
13850    }
13851
13852    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
13853    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
13854    #[inline(never)]
13855    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
13856        use crate::ast::MaintainKind;
13857        self.skip_paren_option_list();
13858        let kind = match self.peek() {
13859            // `TABLE` and `INDEX` lex as keywords, not identifiers.
13860            Token::Table | Token::Index => {
13861                self.advance();
13862                MaintainKind::ReindexRelation
13863            }
13864            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
13865                "index" | "table" => {
13866                    self.advance();
13867                    MaintainKind::ReindexRelation
13868                }
13869                "schema" => {
13870                    self.advance();
13871                    MaintainKind::ReindexSchema
13872                }
13873                "system" | "database" => {
13874                    self.advance();
13875                    MaintainKind::Whole
13876                }
13877                // PG requires the object type; anything else is the
13878                // caller's problem, not something to swallow.
13879                _ => MaintainKind::ReindexRelation,
13880            },
13881            _ => MaintainKind::Whole,
13882        };
13883        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
13884        // allows the plain form, so the modifier is recorded rather than
13885        // skipped. It still has no effect on how the reindex runs.
13886        let mut concurrently = false;
13887        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
13888            self.advance();
13889            concurrently = true;
13890        }
13891        let target = self.take_optional_maintain_name();
13892        self.consume_until_statement_boundary();
13893        Ok(Statement::Maintain {
13894            kind,
13895            concurrently,
13896            target,
13897        })
13898    }
13899
13900    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
13901    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
13902    #[inline(never)]
13903    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
13904        use crate::ast::MaintainKind;
13905        self.skip_paren_option_list();
13906        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
13907            self.advance();
13908        }
13909        let target = self.take_optional_maintain_name();
13910        self.consume_until_statement_boundary();
13911        Ok(Statement::Maintain {
13912            kind: if target.is_some() {
13913                MaintainKind::ClusterRelation
13914            } else {
13915                MaintainKind::Whole
13916            },
13917            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
13918            // transaction block quite happily (measured).
13919            concurrently: false,
13920            target,
13921        })
13922    }
13923
13924    /// The next token as a relation / schema name, when there is one.
13925    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
13926        match self.peek() {
13927            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
13928                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
13929                _ => None,
13930            },
13931            _ => None,
13932        }
13933    }
13934
13935    /// A parenthesised option list, absorbed.
13936    fn skip_paren_option_list(&mut self) {
13937        if !matches!(self.peek(), Token::LParen) {
13938            return;
13939        }
13940        let mut depth = 0usize;
13941        loop {
13942            match self.advance() {
13943                Token::LParen => depth += 1,
13944                Token::RParen => {
13945                    depth -= 1;
13946                    if depth == 0 {
13947                        return;
13948                    }
13949                }
13950                Token::Eof => return,
13951                _ => {}
13952            }
13953        }
13954    }
13955
13956    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
13957    /// column list.
13958    ///
13959    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
13960    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
13961    /// / ALL. The three that describe physical storage have no meaning
13962    /// here, so they parse and change nothing rather than making a
13963    /// dump that mentions them fail to load.
13964    ///
13965    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
13966    /// parse chain the nesting sentinel is tuned against.
13967    #[inline(never)]
13968    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
13969        self.advance(); // LIKE
13970        let source = self.expect_ident_like()?;
13971        let mut options = crate::ast::LikeOptions::default();
13972        loop {
13973            let including = match self.peek() {
13974                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
13975                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
13976                _ => break,
13977            };
13978            self.advance();
13979            // `ALL` lexes as its own keyword, not an identifier.
13980            let opt = if matches!(self.peek(), Token::All) {
13981                self.advance();
13982                alloc::string::String::from("all")
13983            } else {
13984                self.expect_ident_like()?
13985            };
13986            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
13987                o.defaults = on;
13988                o.constraints = on;
13989                o.identity = on;
13990                o.generated = on;
13991                o.indexes = on;
13992                o.comments = on;
13993            };
13994            match opt.to_ascii_lowercase().as_str() {
13995                "all" => set(&mut options, including),
13996                "defaults" => options.defaults = including,
13997                "constraints" => options.constraints = including,
13998                "identity" => options.identity = including,
13999                "generated" => options.generated = including,
14000                "indexes" => options.indexes = including,
14001                "comments" => options.comments = including,
14002                // No storage model to copy into.
14003                "storage" | "statistics" | "compression" => {}
14004                other => {
14005                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14006                }
14007            }
14008        }
14009        Ok(crate::ast::LikeSpec {
14010            source,
14011            at,
14012            options,
14013        })
14014    }
14015
14016    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14017        // Caller already consumed CREATE; we're sitting on TABLE.
14018        debug_assert!(matches!(self.peek(), Token::Table));
14019        self.advance();
14020        let if_not_exists = self.consume_if_not_exists();
14021        let name = self.expect_ident_like()?;
14022        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14023        // child shape has no column list; the child inherits its
14024        // columns from the parent at engine-DDL time. Detect it
14025        // before the `(` requirement below.
14026        if matches!(self.peek(), Token::Partition)
14027            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14028        {
14029            self.advance(); // PARTITION
14030            self.advance(); // of
14031            let partition_of = self.parse_partition_of_tail()?;
14032            return Ok(Statement::CreateTable(CreateTableStatement {
14033                temporary: false,
14034                name,
14035                columns: Vec::new(),
14036                like_specs: Vec::new(),
14037                inherits: Vec::new(),
14038                if_not_exists,
14039                foreign_keys: Vec::new(),
14040                table_constraints: Vec::new(),
14041                partition_by: None,
14042                partition_of: Some(partition_of),
14043            }));
14044        }
14045        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14046        // the materialized-view materialisation path (run the SELECT, infer the
14047        // column types, create + populate the table) but marks the node so the
14048        // executor creates a plain table without a mat-view registry entry.
14049        if matches!(self.peek(), Token::As) {
14050            self.advance();
14051            let body_stmt = self.parse_select_stmt()?;
14052            let Statement::Select(body) = body_stmt else {
14053                return Err(self.err(format!(
14054                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14055                )));
14056            };
14057            let with_data = self.parse_optional_with_data(true)?;
14058            return Ok(Statement::CreateMaterializedView(
14059                crate::ast::CreateMaterializedViewStatement {
14060                    temporary: false,
14061                    name,
14062                    if_not_exists,
14063                    columns: Vec::new(),
14064                    body,
14065                    with_data,
14066                    as_plain_table: true,
14067                },
14068            ));
14069        }
14070        if !matches!(self.peek(), Token::LParen) {
14071            return Err(self.err(format!(
14072                "expected '(' after table name, got {:?}",
14073                self.peek()
14074            )));
14075        }
14076        self.advance();
14077        let mut columns = Vec::new();
14078        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14079        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14080        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14081        loop {
14082            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14083            // column list. It is how a child that adds nothing of its own is
14084            // written, and this loop demanded at least one entry: `syntax
14085            // error at or near ")"`. The child takes the parent's columns,
14086            // which the INHERITS clause already arranges.
14087            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14088                self.advance();
14089                break;
14090            }
14091            // v7.6.0 / v7.9.18 — distinguish table-level constraint
14092            // clauses from column definitions. Constraints start
14093            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14094            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14095            // a column.
14096            if self.peek_table_level_pk_start() {
14097                table_constraints.push(self.parse_table_level_primary_key()?);
14098            } else if matches!(self.peek(), Token::Like) {
14099                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14100                // <opt> ]*`. The source table's shape lives in the catalog,
14101                // so this records the clause and the engine expands it.
14102                like_specs.push(self.parse_create_table_like(columns.len())?);
14103            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14104                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14105                table_constraints.push(self.parse_table_level_exclude()?);
14106            } else if self.peek_table_level_unique_start() {
14107                table_constraints.push(self.parse_table_level_unique()?);
14108            } else if self.peek_table_level_check_start() {
14109                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14110                table_constraints.push(self.parse_table_level_check()?);
14111            } else if self.peek_mysql_inline_key_start() {
14112                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14113                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14114                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14115                // inside the column list. Skip name + paren list;
14116                // for UNIQUE KEY, register as a UC.
14117                if let Some(uc) = self.parse_mysql_inline_key()? {
14118                    table_constraints.push(uc);
14119                }
14120            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14121                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14122                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14123                // CHECK is named, and the named-CONSTRAINT arm used
14124                // to accept FOREIGN KEY only. The name is accepted
14125                // and discarded — same handling as every other SPG
14126                // constraint name.
14127                self.advance(); // CONSTRAINT
14128                // v7.39 (read01 round 48) — the name is kept now: the schema
14129                // stores it, so DROP / RENAME CONSTRAINT can find it.
14130                let con_name = self.expect_ident_like()?;
14131                let mut tc = match kind {
14132                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14133                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14134                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14135                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14136                };
14137                match &mut tc {
14138                    crate::ast::TableConstraint::Check { name, .. }
14139                    | crate::ast::TableConstraint::Unique { name, .. }
14140                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
14141                    | crate::ast::TableConstraint::Exclude { name, .. } => {
14142                        *name = Some(con_name);
14143                    }
14144                    _ => {}
14145                }
14146                table_constraints.push(tc);
14147            } else if self.peek_constraint_or_fk_start() {
14148                foreign_keys.push(self.parse_table_level_fk()?);
14149            } else {
14150                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14151                // v7.13.0 — fold inline UNIQUE / CHECK column
14152                // constraints into table-level entries so the
14153                // engine path stays uniform.
14154                if col.is_unique {
14155                    table_constraints.push(crate::ast::TableConstraint::Unique {
14156                        name: None,
14157                        columns: alloc::vec![col.name.clone()],
14158                        nulls_not_distinct: col.unique_nulls_not_distinct,
14159                        deferrable: col.constraint_deferrable,
14160                        initially_deferred: col.constraint_initially_deferred,
14161                    });
14162                }
14163                if let Some(check_expr) = col.check.clone() {
14164                    table_constraints.push(crate::ast::TableConstraint::Check {
14165                        name: None,
14166                        expr: check_expr,
14167                        not_valid: false,
14168                    });
14169                }
14170                columns.push(col);
14171                if let Some(fk) = col_level_fk {
14172                    foreign_keys.push(fk);
14173                }
14174            }
14175            match self.peek() {
14176                Token::Comma => {
14177                    self.advance();
14178                }
14179                Token::RParen => {
14180                    self.advance();
14181                    break;
14182                }
14183                other => {
14184                    return Err(
14185                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14186                    );
14187                }
14188            }
14189        }
14190        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14191        // `CREATE TABLE k (LIKE t)` is a complete definition even though
14192        // nothing is written between the parentheses.
14193        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14194        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14195        // empty parentheses were a parse error in their own right — quite apart
14196        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14197        // SPG does not have (filed separately).
14198        let _ = &like_specs;
14199        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14200        // It sits between the column list and the MySQL table options,
14201        // and it was a syntax error until this round.
14202        let mut inherits: Vec<String> = Vec::new();
14203        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14204            if k.eq_ignore_ascii_case("inherits"))
14205        {
14206            self.advance();
14207            if !matches!(self.peek(), Token::LParen) {
14208                return Err(self.err(alloc::format!(
14209                    "expected ( after INHERITS, got {:?}",
14210                    self.peek()
14211                )));
14212            }
14213            self.advance();
14214            loop {
14215                inherits.push(self.expect_ident_like()?);
14216                if matches!(self.peek(), Token::Comma) {
14217                    self.advance();
14218                    continue;
14219                }
14220                break;
14221            }
14222            if !matches!(self.peek(), Token::RParen) {
14223                return Err(self.err(alloc::format!(
14224                    "expected ) closing INHERITS, got {:?}",
14225                    self.peek()
14226                )));
14227            }
14228            self.advance();
14229        }
14230        // v7.14.0 — consume MySQL/MariaDB table options after the
14231        // closing `)`. mysqldump emits things like
14232        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14233        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14234        // SPG accepts all forms as no-ops (each option is
14235        // `<ident> [=] <ident-or-string>` separated by whitespace).
14236        self.consume_mysql_table_options();
14237        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14238        // SPG has no per-table reloptions, so accept and ignore them so a
14239        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14240        self.consume_with_reloptions();
14241        // v7.37.6-B — declarative-partition-parent suffix
14242        // (`PARTITION BY RANGE (key_col)`) sits after the column
14243        // list + MySQL table-options. v7.37.6-B only accepts RANGE
14244        // and locks the key column at one ident; the engine then
14245        // verifies the column type is TIMESTAMPTZ.
14246        let partition_by = if matches!(self.peek(), Token::Partition) {
14247            self.advance(); // PARTITION
14248            if !self.peek_is_by() {
14249                return Err(self.err(format!(
14250                    "expected BY after PARTITION, got {:?}",
14251                    self.peek()
14252                )));
14253            }
14254            self.advance();
14255            Some(self.parse_partition_by_tail()?)
14256        } else {
14257            None
14258        };
14259        Ok(Statement::CreateTable(CreateTableStatement {
14260            temporary: false,
14261            name,
14262            columns,
14263            like_specs,
14264            inherits,
14265            if_not_exists,
14266            foreign_keys,
14267            table_constraints,
14268            partition_by,
14269            partition_of: None,
14270        }))
14271    }
14272
14273    /// v7.37.6-B — case-insensitive ident match helper for the
14274    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14275    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14276    /// didn't burn a global keyword slot for each (see the
14277    /// `Token::Partition` doc-comment in `lexer.rs`).
14278    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14279        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14280    }
14281
14282    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14283    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14284    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14285        use crate::ast::{PartitionBySpec, PartitionKindAst};
14286        let kind = match self.peek() {
14287            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14288                self.advance();
14289                PartitionKindAst::Range
14290            }
14291            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14292                self.advance();
14293                PartitionKindAst::List
14294            }
14295            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14296                self.advance();
14297                PartitionKindAst::Hash
14298            }
14299            other => {
14300                return Err(self.err(format!(
14301                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14302                )));
14303            }
14304        };
14305        if !matches!(self.peek(), Token::LParen) {
14306            return Err(self.err(format!(
14307                "expected '(' after PARTITION BY <strategy>, got {:?}",
14308                self.peek()
14309            )));
14310        }
14311        self.advance();
14312        let mut key_columns = Vec::new();
14313        loop {
14314            key_columns.push(self.expect_ident_like()?);
14315            match self.peek() {
14316                Token::Comma => {
14317                    self.advance();
14318                }
14319                Token::RParen => {
14320                    self.advance();
14321                    break;
14322                }
14323                other => {
14324                    return Err(self.err(format!(
14325                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14326                    )));
14327                }
14328            }
14329        }
14330        if key_columns.is_empty() {
14331            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14332        }
14333        Ok(PartitionBySpec { kind, key_columns })
14334    }
14335
14336    /// v7.37.6-B — after `PARTITION OF`, expect
14337    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14338    /// or
14339    ///   <parent> DEFAULT
14340    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14341        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14342        let parent_name = self.expect_ident_like()?;
14343        // v7.37.6-B rejects an explicit column list — the child
14344        // inherits from the parent. mailrs round-7 taught us that
14345        // CREATE TABLE-side schema reconciliation hides drift, so
14346        // we surface this as a parse error rather than silently
14347        // ignoring user columns.
14348        if matches!(self.peek(), Token::LParen) {
14349            return Err(self.err(
14350                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14351                 at v7.37.6-B; the child inherits its columns from the parent"
14352                    .to_string(),
14353            ));
14354        }
14355        let bounds = match self.peek() {
14356            Token::Default => {
14357                self.advance();
14358                PartitionOfBoundsAst::Default
14359            }
14360            Token::For => {
14361                self.advance();
14362                if !matches!(self.peek(), Token::Values) {
14363                    return Err(
14364                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14365                    );
14366                }
14367                self.advance();
14368                // WITH is not a reserved Token in the lexer — it lexes
14369                // as Token::Ident("with"). Disambiguate manually.
14370                let want_with = matches!(
14371                    self.peek(),
14372                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14373                );
14374                if want_with {
14375                    self.advance();
14376                    if !matches!(self.peek(), Token::LParen) {
14377                        return Err(self.err(format!(
14378                            "expected '(' after FOR VALUES WITH, got {:?}",
14379                            self.peek()
14380                        )));
14381                    }
14382                    self.advance();
14383                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14384                    loop {
14385                        let key = self.expect_ident_like()?;
14386                        let n = match self.peek().clone() {
14387                            Token::Integer(v) if u32::try_from(v).is_ok() => {
14388                                self.advance();
14389                                v as u32
14390                            }
14391                            other => {
14392                                return Err(self.err(format!(
14393                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14394                                )));
14395                            }
14396                        };
14397                        match key.to_ascii_uppercase().as_str() {
14398                            "MODULUS" => modulus = Some(n),
14399                            "REMAINDER" => remainder = Some(n),
14400                            other => {
14401                                return Err(self.err(format!(
14402                                    "FOR VALUES WITH: unknown key {other:?}; \
14403                                     expected MODULUS or REMAINDER"
14404                                )));
14405                            }
14406                        }
14407                        match self.peek() {
14408                            Token::Comma => {
14409                                self.advance();
14410                            }
14411                            Token::RParen => {
14412                                self.advance();
14413                                break;
14414                            }
14415                            other => {
14416                                return Err(self.err(format!(
14417                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14418                                )));
14419                            }
14420                        }
14421                    }
14422                    let modulus = modulus
14423                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14424                    let remainder = remainder.ok_or_else(|| {
14425                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14426                    })?;
14427                    if modulus == 0 {
14428                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14429                    }
14430                    if remainder >= modulus {
14431                        return Err(self.err(format!(
14432                            "FOR VALUES WITH: REMAINDER ({remainder}) \
14433                             must be < MODULUS ({modulus})"
14434                        )));
14435                    }
14436                    PartitionOfBoundsAst::Hash { modulus, remainder }
14437                } else {
14438                    match self.peek() {
14439                        Token::From => {
14440                            self.advance();
14441                            let lower = Box::new(self.parse_partition_bound_expr()?);
14442                            if !matches!(self.peek(), Token::To) {
14443                                return Err(self.err(format!(
14444                                    "expected TO after FROM (...), got {:?}",
14445                                    self.peek()
14446                                )));
14447                            }
14448                            self.advance();
14449                            let upper = Box::new(self.parse_partition_bound_expr()?);
14450                            PartitionOfBoundsAst::Range { lower, upper }
14451                        }
14452                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14453                        Token::In => {
14454                            self.advance();
14455                            if !matches!(self.peek(), Token::LParen) {
14456                                return Err(self.err(format!(
14457                                    "expected '(' after FOR VALUES IN, got {:?}",
14458                                    self.peek()
14459                                )));
14460                            }
14461                            self.advance();
14462                            let mut values = Vec::new();
14463                            loop {
14464                                values.push(self.parse_expr(0)?);
14465                                match self.peek() {
14466                                    Token::Comma => {
14467                                        self.advance();
14468                                    }
14469                                    Token::RParen => {
14470                                        self.advance();
14471                                        break;
14472                                    }
14473                                    other => {
14474                                        return Err(self.err(format!(
14475                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14476                                    )));
14477                                    }
14478                                }
14479                            }
14480                            if values.is_empty() {
14481                                return Err(self.err(
14482                                    "FOR VALUES IN requires at least one literal".to_string(),
14483                                ));
14484                            }
14485                            PartitionOfBoundsAst::List { values }
14486                        }
14487                        other => {
14488                            return Err(self.err(format!(
14489                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14490                            )));
14491                        }
14492                    }
14493                }
14494            }
14495            other => {
14496                return Err(self.err(format!(
14497                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14498                )));
14499            }
14500        };
14501        Ok(PartitionOfSpec {
14502            parent_name,
14503            bounds,
14504        })
14505    }
14506
14507    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14508    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14509    /// markers (no-arg builtins) so the engine resolves them
14510    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14511    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14512        if !matches!(self.peek(), Token::LParen) {
14513            return Err(self.err(format!(
14514                "expected '(' before partition bound, got {:?}",
14515                self.peek()
14516            )));
14517        }
14518        self.advance();
14519        let expr = match self.peek() {
14520            Token::Ident(s) | Token::QuotedIdent(s)
14521                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14522            {
14523                let name = s.to_ascii_uppercase();
14524                self.advance();
14525                crate::ast::Expr::FunctionCall {
14526                    name,
14527                    args: Vec::new(),
14528                }
14529            }
14530            _ => self.parse_expr(0)?,
14531        };
14532        if !matches!(self.peek(), Token::RParen) {
14533            return Err(self.err(format!(
14534                "expected ')' after partition bound, got {:?}",
14535                self.peek()
14536            )));
14537        }
14538        self.advance();
14539        Ok(expr)
14540    }
14541
14542    /// v7.14.0 — true when the next tokens look like an inline
14543    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14544    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14545    /// — each followed by an optional name + `(...)`. Critical:
14546    /// a column NAMED `key` / `index` (PG accepts as ident) must
14547    /// NOT be mistaken for the KEY constraint shape. We disambig
14548    /// by requiring the keyword to be followed by either `(` or
14549    /// `<ident> (`.
14550    fn peek_mysql_inline_key_start(&self) -> bool {
14551        let cur = self.peek();
14552        // Shapes:
14553        //   KEY (cols)
14554        //   KEY name (cols)
14555        //   INDEX (cols)
14556        //   INDEX name (cols)
14557        //   UNIQUE KEY [name] (cols)
14558        //   UNIQUE INDEX [name] (cols)
14559        //   FULLTEXT [KEY|INDEX] [name] (cols)
14560        //   SPATIAL [KEY|INDEX] [name] (cols)
14561        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14562            // tokens at skip = the position AFTER the index-form
14563            // keywords (KEY/INDEX) have been consumed.
14564            match self.tokens.get(skip) {
14565                Some(Token::LParen) => true,
14566                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14567                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14568                }
14569                _ => false,
14570            }
14571        };
14572        // `INDEX` lexes as Token::Index (reserved), not as
14573        // Token::Ident("index"). Both shapes count as a KEY/INDEX
14574        // start; the peek helper below handles either.
14575        let is_key_or_index_tok = |t: &Token| -> bool {
14576            matches!(t, Token::Index)
14577                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14578        };
14579        match cur {
14580            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14581            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14582                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14583            }
14584            Token::Ident(s)
14585                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14586            {
14587                let nxt = self.tokens.get(self.pos + 1);
14588                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14589                    self.pos + 2
14590                } else {
14591                    self.pos + 1
14592                };
14593                after_keyword_followed_by_paren_or_ident_paren(after_after)
14594            }
14595            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14596                let nxt = self.tokens.get(self.pos + 1);
14597                if !nxt.is_some_and(is_key_or_index_tok) {
14598                    return false;
14599                }
14600                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14601            }
14602            _ => false,
14603        }
14604    }
14605
14606    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14607    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14608    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14609    /// returns Some(TableConstraint::Index) so the engine builds
14610    /// a real BTree index on the leading column (mysqldump
14611    /// `KEY idx_posts_author (author_id)` shape).
14612    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14613    /// (the storage layer has no matching AM).
14614    fn parse_mysql_inline_key(
14615        &mut self,
14616    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14617        // Detect UNIQUE prefix.
14618        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14619        {
14620            self.advance();
14621            true
14622        } else {
14623            false
14624        };
14625        // Consume FULLTEXT / SPATIAL prefix and record which one
14626        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14627        // dedicated TableConstraint variant so the engine can
14628        // build a tsvector-GIN; SPATIAL still has no matching
14629        // AM, so it falls back to accept-as-no-op.
14630        let mut is_fulltext = false;
14631        let mut is_spatial = false;
14632        if let Token::Ident(s) = self.peek().clone() {
14633            if s.eq_ignore_ascii_case("fulltext") {
14634                self.advance();
14635                is_fulltext = true;
14636            } else if s.eq_ignore_ascii_case("spatial") {
14637                self.advance();
14638                is_spatial = true;
14639            }
14640        }
14641        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14642        // (reserved); accept either token shape.
14643        match self.peek() {
14644            Token::Index => {
14645                self.advance();
14646            }
14647            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14648                self.advance();
14649            }
14650            other => {
14651                return Err(self.err(alloc::format!(
14652                    "expected KEY/INDEX in inline index declaration, got {other:?}"
14653                )));
14654            }
14655        }
14656        // Optional index name (an ident before the `(`).
14657        // v7.15.0 — capture the name when present so the engine
14658        // builds the secondary index under the user's chosen
14659        // name (matches mysqldump's `KEY idx_x (col)` shape).
14660        let mut idx_name: Option<String> = None;
14661        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
14662            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
14663        {
14664            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
14665                idx_name = Some(s);
14666            }
14667        }
14668        // Optional `USING BTREE` / `USING HASH` (MySQL).
14669        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14670            self.advance();
14671            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14672                self.advance();
14673            }
14674        }
14675        // Required column list `(col [, col]*)`.
14676        if !matches!(self.peek(), Token::LParen) {
14677            return Err(self.err(alloc::format!(
14678                "expected '(' in inline KEY/INDEX, got {:?}",
14679                self.peek()
14680            )));
14681        }
14682        self.advance();
14683        let mut cols: Vec<String> = Vec::new();
14684        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
14685            self.advance();
14686            cols.push(s);
14687            // Skip optional `(length)` per-column prefix.
14688            if matches!(self.peek(), Token::LParen) {
14689                let mut depth = 1usize;
14690                self.advance();
14691                while depth > 0 {
14692                    match self.peek() {
14693                        Token::LParen => depth += 1,
14694                        Token::RParen => depth -= 1,
14695                        Token::Eof => break,
14696                        _ => {}
14697                    }
14698                    self.advance();
14699                }
14700            }
14701            // Skip optional ASC / DESC.
14702            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
14703                || matches!(self.peek(), Token::Asc | Token::Desc)
14704            {
14705                self.advance();
14706            }
14707            if matches!(self.peek(), Token::Comma) {
14708                self.advance();
14709                continue;
14710            }
14711            break;
14712        }
14713        if matches!(self.peek(), Token::RParen) {
14714            self.advance();
14715        }
14716        // Trailing options on the inline index — comment / etc.
14717        // Skip until comma or `)`.
14718        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
14719            self.advance();
14720        }
14721        if cols.is_empty() {
14722            return Ok(None);
14723        }
14724        if is_unique {
14725            // Carry the captured idx_name on UNIQUE too so future
14726            // engine work can name the underlying BTree
14727            // accordingly; today the unique-constraint installer
14728            // synthesises the name itself, but Display round-trip
14729            // benefits from preserving it.
14730            Ok(Some(crate::ast::TableConstraint::Unique {
14731                name: idx_name,
14732                columns: cols,
14733                nulls_not_distinct: false,
14734                // MySQL inline UNIQUE KEY has no deferral vocabulary.
14735                deferrable: false,
14736                initially_deferred: false,
14737            }))
14738        } else if is_fulltext {
14739            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
14740            // routes through `TableConstraint::FulltextIndex`;
14741            // the engine builds a tsvector-GIN over each named
14742            // column so MATCH AGAINST gets a real inverted
14743            // index instead of a silently-dropped declaration.
14744            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
14745                name: idx_name,
14746                columns: cols,
14747            }))
14748        } else if is_spatial {
14749            // SPG has no native SPATIAL AM. Accept-as-no-op
14750            // (declaration is parsed, but no index is built).
14751            Ok(None)
14752        } else {
14753            // v7.15.0 — plain KEY / INDEX builds a real BTree
14754            // secondary index.
14755            Ok(Some(crate::ast::TableConstraint::Index {
14756                name: idx_name,
14757                columns: cols,
14758            }))
14759        }
14760    }
14761
14762    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
14763    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
14764    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
14765    /// (in any order, separated by whitespace).
14766    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
14767    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
14768    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
14769    /// bare ident here, and only the parenthesised form is reloptions (so this
14770    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
14771    fn consume_with_reloptions(&mut self) {
14772        let is_with = matches!(
14773            self.peek(),
14774            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14775        );
14776        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
14777            return;
14778        }
14779        self.advance(); // WITH
14780        self.advance(); // (
14781        let mut depth = 1u32;
14782        while depth > 0 && !matches!(self.peek(), Token::Eof) {
14783            match self.peek() {
14784                Token::LParen => depth += 1,
14785                Token::RParen => depth -= 1,
14786                _ => {}
14787            }
14788            self.advance();
14789        }
14790    }
14791
14792    fn consume_mysql_table_options(&mut self) {
14793        loop {
14794            // Heuristic: a table option is an ident (or `DEFAULT`
14795            // reserved keyword) followed by `=` and an
14796            // ident / string / integer.
14797            let name_lc = match self.peek().clone() {
14798                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
14799                Token::Default => alloc::string::String::from("default"),
14800                _ => break,
14801            };
14802            let known = matches!(
14803                name_lc.as_str(),
14804                "engine"
14805                    | "default"
14806                    | "charset"
14807                    | "collate"
14808                    | "auto_increment"
14809                    | "row_format"
14810                    | "comment"
14811                    | "pack_keys"
14812                    | "stats_persistent"
14813                    | "stats_auto_recalc"
14814                    | "stats_sample_pages"
14815                    | "key_block_size"
14816                    | "tablespace"
14817                    | "min_rows"
14818                    | "max_rows"
14819                    | "checksum"
14820                    | "delay_key_write"
14821                    | "insert_method"
14822                    | "data"
14823                    | "index"
14824                    | "encryption"
14825                    | "compression"
14826            );
14827            if !known {
14828                break;
14829            }
14830            self.advance(); // option name
14831            // `DEFAULT` optional prefix is followed by `CHARSET` /
14832            // `COLLATE`; consume the next ident too.
14833            if name_lc == "default" {
14834                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14835                    self.advance();
14836                }
14837            }
14838            if matches!(self.peek(), Token::Eq) {
14839                self.advance();
14840            }
14841            match self.peek() {
14842                Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
14843                    self.advance();
14844                }
14845                _ => {}
14846            }
14847        }
14848    }
14849
14850    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
14851    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
14852    /// sure (otherwise a column literally named `primary` would
14853    /// be mistaken).
14854    fn peek_table_level_pk_start(&self) -> bool {
14855        let cur = self.peek();
14856        let nxt = self.tokens.get(self.pos + 1);
14857        let nxt2 = self.tokens.get(self.pos + 2);
14858        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
14859        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
14860        let is_lparen = matches!(nxt2, Some(Token::LParen));
14861        is_primary && is_key && is_lparen
14862    }
14863
14864    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
14865    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
14866    /// (mailrs round-5 G10).
14867    fn peek_table_level_unique_start(&self) -> bool {
14868        let cur = self.peek();
14869        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
14870        if !is_unique {
14871            return false;
14872        }
14873        let n1 = self.tokens.get(self.pos + 1);
14874        // Plain `UNIQUE (…)`.
14875        if matches!(n1, Some(Token::LParen)) {
14876            return true;
14877        }
14878        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
14879        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
14880        if !is_nulls {
14881            return false;
14882        }
14883        let n2 = self.tokens.get(self.pos + 2);
14884        let n3 = self.tokens.get(self.pos + 3);
14885        let n4 = self.tokens.get(self.pos + 4);
14886        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
14887        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
14888            return true;
14889        }
14890        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
14891        if matches!(n2, Some(Token::Not))
14892            && matches!(n3, Some(Token::Distinct))
14893            && matches!(n4, Some(Token::LParen))
14894        {
14895            return true;
14896        }
14897        false
14898    }
14899
14900    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14901        self.advance(); // PRIMARY
14902        self.advance(); // KEY
14903        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
14904        // v7.39 (round 711) — the trailer's values are CARRIED now; round
14905        // 621 consumed and dropped them (the storing half of F08).
14906        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
14907        Ok(crate::ast::TableConstraint::PrimaryKey {
14908            name: None,
14909            columns,
14910            deferrable,
14911            initially_deferred,
14912        })
14913    }
14914
14915    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14916        self.advance(); // UNIQUE
14917        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
14918        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
14919        // is `NULLS DISTINCT` per the SQL standard.
14920        let mut nulls_not_distinct = false;
14921        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
14922            let n1 = self.tokens.get(self.pos + 1);
14923            let n2 = self.tokens.get(self.pos + 2);
14924            let is_not = matches!(n1, Some(Token::Not));
14925            let is_distinct = matches!(n2, Some(Token::Distinct));
14926            if is_not && is_distinct {
14927                self.advance(); // NULLS
14928                self.advance(); // NOT
14929                self.advance(); // DISTINCT
14930                nulls_not_distinct = true;
14931            } else if matches!(n1, Some(Token::Distinct)) {
14932                self.advance(); // NULLS
14933                self.advance(); // DISTINCT
14934            }
14935        }
14936        let columns = self.parse_paren_ident_list("UNIQUE")?;
14937        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
14938        Ok(crate::ast::TableConstraint::Unique {
14939            name: None,
14940            columns,
14941            nulls_not_distinct,
14942            deferrable,
14943            initially_deferred,
14944        })
14945    }
14946
14947    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
14948    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
14949    /// expression.
14950    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
14951    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
14952    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
14953    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
14954    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
14955    /// commit: `NOT` starts no other suffix here, but reading both
14956    /// tokens before advancing keeps the caller's error message intact
14957    /// if someone writes `NOT NULL` by mistake.
14958    fn parse_not_valid_suffix(&mut self) -> bool {
14959        if !matches!(self.peek(), Token::Not) {
14960            return false;
14961        }
14962        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
14963        {
14964            return false;
14965        }
14966        self.advance();
14967        self.advance();
14968        true
14969    }
14970
14971    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14972        self.advance(); // EXCLUDE
14973        // Optional `USING <method>`.
14974        let mut method = None;
14975        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14976            self.advance();
14977            method = Some(match self.advance() {
14978                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
14979                other => {
14980                    return Err(self.err(alloc::format!(
14981                        "expected index method after USING, got {other:?}"
14982                    )));
14983                }
14984            });
14985        }
14986        if !matches!(self.peek(), Token::LParen) {
14987            return Err(self.err(alloc::format!(
14988                "expected '(' after EXCLUDE, got {:?}",
14989                self.peek()
14990            )));
14991        }
14992        self.advance();
14993        let mut elements: Vec<(String, String)> = Vec::new();
14994        loop {
14995            let col = match self.advance() {
14996                Token::Ident(s) | Token::QuotedIdent(s) => s,
14997                other => {
14998                    return Err(self.err(alloc::format!(
14999                        "expected column name in EXCLUDE, got {other:?}"
15000                    )));
15001                }
15002            };
15003            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15004                return Err(self.err(alloc::format!(
15005                    "expected WITH after EXCLUDE column, got {:?}",
15006                    self.peek()
15007                )));
15008            }
15009            self.advance();
15010            let op = match self.advance() {
15011                Token::InetOverlap => String::from("&&"),
15012                Token::Intersects => String::from("?#"),
15013                Token::IsBelow => String::from("<^"),
15014                Token::IsAbove => String::from(">^"),
15015                Token::PatternLt => String::from("~<~"),
15016                Token::PatternLtEq => String::from("~<=~"),
15017                Token::PatternGt => String::from("~>~"),
15018                Token::PatternGtEq => String::from("~>=~"),
15019                Token::TsMatchOld => String::from("@@@"),
15020                Token::Eq => String::from("="),
15021                Token::JsonContains => String::from("@>"),
15022                Token::JsonContainedBy => String::from("<@"),
15023                Token::OverLeft => String::from("&<"),
15024                Token::OverRight => String::from("&>"),
15025                other => {
15026                    return Err(self.err(alloc::format!(
15027                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15028                    )));
15029                }
15030            };
15031            elements.push((col, op));
15032            if matches!(self.peek(), Token::Comma) {
15033                self.advance();
15034                continue;
15035            }
15036            break;
15037        }
15038        if !matches!(self.peek(), Token::RParen) {
15039            return Err(self.err(alloc::format!(
15040                "expected ')' to close EXCLUDE, got {:?}",
15041                self.peek()
15042            )));
15043        }
15044        self.advance();
15045        Ok(crate::ast::TableConstraint::Exclude {
15046            name: None,
15047            method,
15048            elements,
15049        })
15050    }
15051
15052    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15053        self.advance(); // CHECK
15054        if !matches!(self.peek(), Token::LParen) {
15055            return Err(self.err(alloc::format!(
15056                "expected '(' after CHECK, got {:?}",
15057                self.peek()
15058            )));
15059        }
15060        self.advance();
15061        let expr = self.parse_expr(0)?;
15062        if !matches!(self.peek(), Token::RParen) {
15063            return Err(self.err(alloc::format!(
15064                "expected ')' to close CHECK predicate, got {:?}",
15065                self.peek()
15066            )));
15067        }
15068        self.advance();
15069        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15070        // are no existing rows for PG to skip, so it rejects the suffix.
15071        Ok(crate::ast::TableConstraint::Check {
15072            name: None,
15073            expr,
15074            not_valid: false,
15075        })
15076    }
15077
15078    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15079    fn peek_table_level_check_start(&self) -> bool {
15080        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15081    }
15082
15083    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15084    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15085    /// on the dedicated FK path (`parse_table_level_fk` consumes its
15086    /// own CONSTRAINT prefix).
15087    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15088        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15089            return None;
15090        }
15091        // tokens[pos+1] is the constraint name (any ident-like);
15092        // tokens[pos+2] is the kind keyword.
15093        match self.tokens.get(self.pos + 2) {
15094            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15095                Some(NamedTableConstraintKind::Check)
15096            }
15097            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15098                Some(NamedTableConstraintKind::Unique)
15099            }
15100            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15101                Some(NamedTableConstraintKind::PrimaryKey)
15102            }
15103            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15104                Some(NamedTableConstraintKind::Exclude)
15105            }
15106            _ => None,
15107        }
15108    }
15109
15110    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15111        if !matches!(self.peek(), Token::LParen) {
15112            return Err(self.err(alloc::format!(
15113                "expected '(' after {ctx}, got {:?}",
15114                self.peek()
15115            )));
15116        }
15117        self.advance();
15118        let mut out = Vec::new();
15119        loop {
15120            out.push(self.expect_ident_like()?);
15121            match self.peek() {
15122                Token::Comma => {
15123                    self.advance();
15124                }
15125                Token::RParen => {
15126                    self.advance();
15127                    break;
15128                }
15129                other => {
15130                    return Err(self.err(alloc::format!(
15131                        "expected ',' or ')' in {ctx} list, got {other:?}"
15132                    )));
15133                }
15134            }
15135        }
15136        if out.is_empty() {
15137            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15138        }
15139        Ok(out)
15140    }
15141
15142    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15143    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15144    /// table-level FK; a column def never starts with either keyword
15145    /// (column names are not in this reserved set).
15146    fn peek_constraint_or_fk_start(&self) -> bool {
15147        let is_constraint_kw = matches!(
15148            self.peek(),
15149            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15150        );
15151        let is_foreign_kw = matches!(
15152            self.peek(),
15153            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15154        );
15155        is_constraint_kw || is_foreign_kw
15156    }
15157
15158    /// v7.6.0 — parse a table-level FK clause:
15159    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15160    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15161    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15162        let mut name: Option<String> = None;
15163        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15164            self.advance();
15165            name = Some(self.expect_ident_like()?);
15166        }
15167        // `FOREIGN`
15168        match self.advance() {
15169            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15170            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15171        }
15172        // `KEY`
15173        match self.advance() {
15174            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15175            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15176        }
15177        // `(col, col, ...)`
15178        if !matches!(self.peek(), Token::LParen) {
15179            return Err(self.err(format!(
15180                "expected '(' after FOREIGN KEY, got {:?}",
15181                self.peek()
15182            )));
15183        }
15184        self.advance();
15185        let mut columns = Vec::new();
15186        loop {
15187            columns.push(self.expect_ident_like()?);
15188            match self.peek() {
15189                Token::Comma => {
15190                    self.advance();
15191                }
15192                Token::RParen => {
15193                    self.advance();
15194                    break;
15195                }
15196                other => {
15197                    return Err(self.err(format!(
15198                        "expected ',' or ')' in FK column list, got {other:?}"
15199                    )));
15200                }
15201            }
15202        }
15203        if columns.is_empty() {
15204            return Err(self.err("FOREIGN KEY requires at least one column".into()));
15205        }
15206        let (
15207            parent_table,
15208            parent_columns,
15209            on_delete,
15210            on_update,
15211            match_type,
15212            deferrable,
15213            initially_deferred,
15214        ) = self.parse_references_tail(columns.len())?;
15215        Ok(ForeignKeyConstraint {
15216            name,
15217            columns,
15218            parent_table,
15219            parent_columns,
15220            on_delete,
15221            on_update,
15222            match_type,
15223            deferrable,
15224            initially_deferred,
15225        })
15226    }
15227
15228    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15229    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15230    /// the local column count, used to default the parent column
15231    /// list when omitted (SQL spec: parent's PK is implied).
15232    fn parse_references_tail(
15233        &mut self,
15234        expected_arity: usize,
15235    ) -> Result<
15236        (
15237            String,
15238            Vec<String>,
15239            FkAction,
15240            FkAction,
15241            crate::ast::MatchType,
15242            // v7.39 (round 288) — deferrable, initially_deferred.
15243            bool,
15244            bool,
15245        ),
15246        ParseError,
15247    > {
15248        match self.advance() {
15249            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15250            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15251        }
15252        let parent_table = self.expect_ident_like()?;
15253        let mut parent_columns: Vec<String> = Vec::new();
15254        if matches!(self.peek(), Token::LParen) {
15255            self.advance();
15256            loop {
15257                parent_columns.push(self.expect_ident_like()?);
15258                match self.peek() {
15259                    Token::Comma => {
15260                        self.advance();
15261                    }
15262                    Token::RParen => {
15263                        self.advance();
15264                        break;
15265                    }
15266                    other => {
15267                        return Err(self.err(format!(
15268                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
15269                        )));
15270                    }
15271                }
15272            }
15273        }
15274        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15275            return Err(self.err(format!(
15276                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15277                expected_arity,
15278                parent_columns.len()
15279            )));
15280        }
15281        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15282        // it between the referenced column list and the ON / DEFERRABLE
15283        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15284        // is skipped when any referencing column is NULL), so SIMPLE —
15285        // the default, and the only spelling pg_dump emits — is accepted
15286        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15287        // mixed-NULL rule, which is not wired yet; reject them honestly
15288        // rather than silently applying SIMPLE (PG itself errors on
15289        // MATCH PARTIAL as "not yet implemented").
15290        let mut match_type = crate::ast::MatchType::Simple;
15291        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15292            self.advance();
15293            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15294            // SIMPLE / PARTIAL arrive as bare identifiers.
15295            let kind = match self.advance() {
15296                Token::Full => "FULL".to_string(),
15297                Token::Ident(s) => s.to_uppercase(),
15298                other => {
15299                    return Err(self.err(format!(
15300                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15301                    )));
15302                }
15303            };
15304            match kind.as_str() {
15305                "SIMPLE" => {} // Default — match_type stays Simple.
15306                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15307                // when ALL referencing columns are NULL; a mixed-NULL key errors.
15308                "FULL" => match_type = crate::ast::MatchType::Full,
15309                "PARTIAL" => {
15310                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15311                }
15312                _ => {
15313                    return Err(self.err(format!(
15314                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15315                    )));
15316                }
15317            }
15318        }
15319        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15320        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15321        // <action>` / `ON UPDATE <action>` in either order. PG /
15322        // pg_dump emits the timing clause AFTER the ON clauses
15323        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15324        // but the SQL spec allows either order. We loop over
15325        // every possible trailer and dispatch on the next token,
15326        // stopping when nothing matches. Phase 3.1 changes the
15327        // bare DEFERRABLE form from hard-error to accept-as-
15328        // immediate; SPG is single-writer with no deferred-
15329        // constraint window so the runtime semantics are always
15330        // immediate even when INITIALLY DEFERRED is requested.
15331        // PG's default referential action (no ON DELETE / ON UPDATE
15332        // clause) is NO ACTION, not RESTRICT — the two enforce
15333        // identically in SPG (single-writer, no deferred window; see the
15334        // shared match arm in constraints.rs) but information_schema.
15335        // referential_constraints must report NO ACTION to match PG.
15336        let mut on_delete = FkAction::NoAction;
15337        let mut on_update = FkAction::NoAction;
15338        let mut seen_on_delete = false;
15339        let mut seen_on_update = false;
15340        let mut deferrable = false;
15341        let mut initially_deferred = false;
15342        loop {
15343            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15344            let before = self.pos;
15345            let (d, idef) = self.consume_deferrable_clauses_timed()?;
15346            if self.pos != before {
15347                deferrable = d;
15348                initially_deferred = idef;
15349                continue;
15350            }
15351            // ON DELETE / ON UPDATE.
15352            if !matches!(self.peek(), Token::On) {
15353                break;
15354            }
15355            self.advance();
15356            let which = self.advance();
15357            let action = self.parse_fk_action()?;
15358            match which {
15359                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15360                    if seen_on_delete {
15361                        return Err(self.err("ON DELETE specified twice".into()));
15362                    }
15363                    seen_on_delete = true;
15364                    on_delete = action;
15365                }
15366                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15367                    if seen_on_update {
15368                        return Err(self.err("ON UPDATE specified twice".into()));
15369                    }
15370                    seen_on_update = true;
15371                    on_update = action;
15372                }
15373                other => {
15374                    return Err(
15375                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15376                    );
15377                }
15378            }
15379        }
15380        Ok((
15381            parent_table,
15382            parent_columns,
15383            on_delete,
15384            on_update,
15385            match_type,
15386            deferrable,
15387            initially_deferred,
15388        ))
15389    }
15390
15391    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15392    /// NO ACTION`.
15393    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15394        match self.advance() {
15395            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15396            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15397            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15398                Token::Null => Ok(FkAction::SetNull),
15399                Token::Default => Ok(FkAction::SetDefault),
15400                other => Err(self.err(format!(
15401                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15402                ))),
15403            },
15404            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15405                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15406                other => Err(self.err(format!(
15407                    "expected ACTION after NO in FK action, got {other:?}"
15408                ))),
15409            },
15410            other => Err(self.err(format!(
15411                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15412            ))),
15413        }
15414    }
15415
15416    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15417    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15418    fn consume_if_not_exists(&mut self) -> bool {
15419        // `IF` arrives as a bare Ident (we don't reserve it because it
15420        // also appears mid-expression in PG, though we don't support
15421        // those forms yet).
15422        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15423        if !looks_like_if {
15424            return false;
15425        }
15426        // Peek one ahead before committing: only consume IF when it's
15427        // actually `IF NOT EXISTS`.
15428        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15429            return false;
15430        }
15431        if !matches!(
15432            self.tokens.get(self.pos + 2),
15433            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15434        ) {
15435            return false;
15436        }
15437        self.advance(); // IF
15438        self.advance(); // NOT
15439        self.advance(); // EXISTS
15440        true
15441    }
15442
15443    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15444    /// Consumes IF EXISTS as a pair; returns false otherwise
15445    /// without consuming any tokens.
15446    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15447    /// ENABLE/DISABLE/FORCE/NO FORCE.
15448    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15449        for kw in ["row", "level", "security"] {
15450            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15451            {
15452                return Err(self.err(alloc::format!(
15453                    "expected {} in ROW LEVEL SECURITY, got {:?}",
15454                    kw.to_ascii_uppercase(),
15455                    self.peek()
15456                )));
15457            }
15458            self.advance();
15459        }
15460        Ok(())
15461    }
15462
15463    fn consume_if_exists(&mut self) -> bool {
15464        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15465        if !looks_like_if {
15466            return false;
15467        }
15468        if !matches!(
15469            self.tokens.get(self.pos + 1),
15470            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15471        ) {
15472            return false;
15473        }
15474        self.advance(); // IF
15475        self.advance(); // EXISTS
15476        true
15477    }
15478
15479    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15480    /// qualifiers after an index column ref. ASC / DESC are
15481    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15482    /// We accept and discard them since single-column BTree
15483    /// stores rows in natural key order today.
15484    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15485    /// ORDER BY key. Returns None when absent.
15486    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15487        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15488            return Ok(None);
15489        }
15490        self.advance();
15491        match self.advance() {
15492            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15493            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15494            other => Err(self.err(alloc::format!(
15495                "expected FIRST or LAST after NULLS, got {other:?}"
15496            ))),
15497        }
15498    }
15499
15500    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15501    /// rather than discarded.
15502    ///
15503    /// SPG's index does not scan in a direction — column ordering is
15504    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15505    /// reproduction of the DDL, and dropping the clause meant
15506    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15507    /// dump lost it, and a schema diff saw drift on every run.
15508    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15509        let mut order = crate::ast::IndexColumnOrder::default();
15510        loop {
15511            match self.peek() {
15512                Token::Asc => {
15513                    self.advance();
15514                }
15515                Token::Desc => {
15516                    order.descending = true;
15517                    self.advance();
15518                }
15519                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15520                    let look = self.tokens.get(self.pos + 1);
15521                    if matches!(
15522                        look,
15523                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15524                            || k.eq_ignore_ascii_case("last")
15525                    ) {
15526                        self.advance();
15527                        order.nulls_first = Some(matches!(
15528                            self.advance(),
15529                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
15530                        ));
15531                    } else {
15532                        break;
15533                    }
15534                }
15535                _ => break,
15536            }
15537        }
15538        order
15539    }
15540
15541    fn parse_create_index_stmt_after_create(
15542        &mut self,
15543        is_unique: bool,
15544    ) -> Result<Statement, ParseError> {
15545        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15546        debug_assert!(matches!(self.peek(), Token::Index));
15547        self.advance();
15548        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15549        // SPG's CREATE INDEX is synchronous end-to-end today (real
15550        // CONCURRENTLY variant with restartable scans queues with
15551        // v7.39 indexes epic), so the modifier has no runtime effect
15552        // — same accept-and-no-op shape as v7.37.16.5 DETACH
15553        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15554        // VIEW CONCURRENTLY.
15555        let mut concurrently = false;
15556        if matches!(
15557            self.peek(),
15558            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15559        ) {
15560            self.advance();
15561            concurrently = true;
15562        }
15563        let if_not_exists = self.consume_if_not_exists();
15564        // v7.39 (read01 round 93) — the index name is optional (PG since
15565        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15566        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15567        // was given; leave it empty and the engine derives a PG-style
15568        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15569        let name = if matches!(self.peek(), Token::On) {
15570            String::new()
15571        } else {
15572            self.expect_ident_like()?
15573        };
15574        if !matches!(self.peek(), Token::On) {
15575            return Err(self.err(format!(
15576                "expected ON after CREATE INDEX <name>, got {:?}",
15577                self.peek()
15578            )));
15579        }
15580        self.advance();
15581        let table = self.expect_ident_like()?;
15582        // Optional `USING <method>` — only recognised method in v2.0 is
15583        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15584        // ident `using` (we don't promote it to a reserved keyword
15585        // because it isn't reserved anywhere else in our SQL surface).
15586        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15587            self.advance();
15588            let m = self.expect_ident_like()?;
15589            match m.to_ascii_lowercase().as_str() {
15590                "hnsw" => IndexMethod::Hnsw,
15591                "btree" => IndexMethod::BTree,
15592                "brin" => IndexMethod::Brin,
15593                // v7.12.3 — real GIN inverted index over `tsvector`.
15594                // v7.9.26b's `USING gin` → BTree silent fallback is
15595                // gone; the engine validates that the indexed column
15596                // is `tsvector` at CREATE INDEX time.
15597                "gin" => IndexMethod::Gin,
15598                // v7.9.26b — PG `pg_dump` emits `USING gist` /
15599                // `USING spgist` / `USING hash` for their built-in
15600                // AMs that SPG doesn't have a matching
15601                // implementation for; degrade to BTree on the
15602                // leading column so the schema loads + the index
15603                // catalogue stays consistent. Operator pays the
15604                // planner cost only for the queries that would have
15605                // used the specialised AM.
15606                "gist" | "spgist" | "hash" => IndexMethod::BTree,
15607                // v7.11.3 — pgvector ships both `ivfflat` and
15608                // `hnsw`. Customers shouldn't have to choose
15609                // their on-disk index method based on what SPG
15610                // implements; accept `ivfflat` as a synonym for
15611                // `hnsw` so PG schemas using either method drop
15612                // in. The vector distance op (`<->` / `<#>` /
15613                // `<=>`) at query time still picks the metric.
15614                "ivfflat" => IndexMethod::Hnsw,
15615                other => {
15616                    return Err(self.err(alloc::format!(
15617                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15618                    )));
15619                }
15620            }
15621        } else {
15622            IndexMethod::BTree
15623        };
15624        if !matches!(self.peek(), Token::LParen) {
15625            return Err(self.err(format!(
15626                "expected '(' before indexed column, got {:?}",
15627                self.peek()
15628            )));
15629        }
15630        self.advance();
15631        // v6.8.2 — accept either a bare column ident (legacy) or
15632        // an expression `fn(col, …)` for expression indexes.
15633        // Distinguish by peeking the token *after* the current
15634        // ident: `ident )` is the legacy column-only path;
15635        // anything else triggers the Pratt expression parser.
15636        // (`advance()` uses `mem::replace` to nil out the current
15637        // slot, so we can't save+rewind cleanly — peek-ahead via
15638        // direct index avoids the mutation.)
15639        let mut opclass: Option<String> = None;
15640        let mut key_collation: Option<String> = None;
15641        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
15642            // Single column with `)` immediately after — fast path.
15643            // v7.9.29 — also: bare column followed by `,` (the
15644            // multi-column form `(a, b, c)`). Without this branch
15645            // the leading ident gets pulled into `parse_expr`
15646            // which then sets `expression = Some(Column(a))` and
15647            // breaks Display round-trip on the multi-column shape.
15648            Token::Ident(s) | Token::QuotedIdent(s)
15649                if matches!(
15650                    self.tokens.get(self.pos + 1),
15651                    Some(Token::RParen | Token::Comma)
15652                ) =>
15653            {
15654                self.advance();
15655                (s, None)
15656            }
15657            // v7.9.22 — single column followed by a pgvector
15658            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
15659            // v7.15.0 — capture the opclass instead of discarding
15660            // it so the engine can dispatch (e.g. `gin_trgm_ops`
15661            // → real trigram-shingle GIN over a TEXT column).
15662            // Vector/HNSW opclasses still take their distance
15663            // metric from the query operator (`<->` / `<#>` /
15664            // `<=>`), so for those callers the opclass stays
15665            // informational.
15666            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
15667            // opclass: `(embedding public.vector_cosine_ops)`. Strip
15668            // the schema and dispatch on the bare opclass, the same
15669            // treatment table/type names get.
15670            Token::Ident(s) | Token::QuotedIdent(s)
15671                if matches!(
15672                    self.tokens.get(self.pos + 1),
15673                    Some(Token::Ident(_) | Token::QuotedIdent(_))
15674                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
15675                    && matches!(
15676                        self.tokens.get(self.pos + 3),
15677                        Some(Token::Ident(op) | Token::QuotedIdent(op))
15678                            if is_vector_opclass_name(op)
15679                    ) =>
15680            {
15681                self.advance(); // column name
15682                self.advance(); // schema qualifier
15683                self.advance(); // dot
15684                let op_tok = self.advance();
15685                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15686                    opclass = Some(op.to_ascii_lowercase());
15687                }
15688                (s, None)
15689            }
15690            Token::Ident(s) | Token::QuotedIdent(s)
15691                if matches!(
15692                    self.tokens.get(self.pos + 1),
15693                    Some(Token::Ident(op) | Token::QuotedIdent(op))
15694                        if is_vector_opclass_name(op)
15695                ) =>
15696            {
15697                self.advance(); // column name
15698                // Capture the opclass token, lower-cased for
15699                // case-insensitive engine dispatch.
15700                let op_tok = self.advance();
15701                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15702                    opclass = Some(op.to_ascii_lowercase());
15703                }
15704                (s, None)
15705            }
15706            Token::Ident(_) | Token::QuotedIdent(_) => {
15707                // v7.39 (round 538) — an explicit COLLATE on the key,
15708                // read by LOOKAHEAD because `parse_expr` absorbs the
15709                // clause as a no-op (SPG orders text by bytes, which is
15710                // the C collation, so it changes nothing to honour). PG
15711                // still PRINTS it: an explicitly written `"C"` and the
15712                // collation a column inherits are different collation
15713                // OBJECTS even where they sort identically, which is why
15714                // `(a COLLATE "C")` shows on a C-collation database too.
15715                if matches!(
15716                    self.tokens.get(self.pos + 1),
15717                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
15718                ) {
15719                    key_collation = match self.tokens.get(self.pos + 2) {
15720                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
15721                            Some(n.clone())
15722                        }
15723                        _ => None,
15724                    };
15725                }
15726                let key_expr = self.parse_expr(0)?;
15727                let primary = extract_first_column(&key_expr).ok_or_else(|| {
15728                    self.err("expression index key must reference at least one column".into())
15729                })?;
15730                (primary, Some(key_expr))
15731            }
15732            // v7.37.43-T4 — parenthesised expression index key
15733            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
15734            // PG's CREATE INDEX requires the expression to be in
15735            // its own parens to disambiguate function calls from
15736            // column lists, so this `LParen` is the inner open-paren
15737            // of an expression key. parse_expr handles the recursive
15738            // descent and consumes the matching `RParen`.
15739            Token::LParen => {
15740                let key_expr = self.parse_expr(0)?;
15741                let primary = extract_first_column(&key_expr).ok_or_else(|| {
15742                    self.err("expression index key must reference at least one column".into())
15743                })?;
15744                (primary, Some(key_expr))
15745            }
15746            other => {
15747                return Err(self.err(format!(
15748                    "expected column ident or expression, got {other:?}"
15749                )));
15750            }
15751        };
15752        // v7.9.14 — accept extra comma-separated columns inside
15753        // the index key parens (`CREATE INDEX … (a, b, c)`).
15754        // mailrs F2. Each extra column may carry an optional
15755        // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
15756        // — parsed and discarded; SPG doesn't honour direction
15757        // on a BTree index today (column ordering is intrinsic
15758        // to the storage). v7.10 will widen to genuine composite
15759        // index keys.
15760        let mut extra_columns: Vec<String> = Vec::new();
15761        // The leading column may also have ASC/DESC after it — and that
15762        // one is the column SPG indexes, so its clause is kept.
15763        let key_order = self.consume_optional_index_column_qualifiers();
15764        while matches!(self.peek(), Token::Comma) {
15765            self.advance();
15766            let extra = self.expect_ident_like()?;
15767            let _ = self.consume_optional_index_column_qualifiers();
15768            extra_columns.push(extra);
15769        }
15770        if !matches!(self.peek(), Token::RParen) {
15771            return Err(self.err(format!(
15772                "expected ')' after indexed column / expression, got {:?}",
15773                self.peek()
15774            )));
15775        }
15776        self.advance();
15777        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
15778        // index-only-scan annotation. Bare ident (not a reserved
15779        // keyword) so we test by case-insensitive string match.
15780        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
15781        {
15782            self.advance();
15783            if !matches!(self.peek(), Token::LParen) {
15784                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
15785            }
15786            self.advance();
15787            let mut cols = Vec::new();
15788            loop {
15789                cols.push(self.expect_ident_like()?);
15790                match self.peek() {
15791                    Token::Comma => {
15792                        self.advance();
15793                    }
15794                    Token::RParen => {
15795                        self.advance();
15796                        break;
15797                    }
15798                    other => {
15799                        return Err(self.err(format!(
15800                            "expected ',' or ')' in INCLUDE list, got {other:?}"
15801                        )));
15802                    }
15803                }
15804            }
15805            cols
15806        } else {
15807            Vec::new()
15808        };
15809        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
15810        // storage parameters. pgvector emits `WITH (lists = N)` for
15811        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
15812        // SPG's HNSW picks its own parameters today (tunable via
15813        // env vars), so the WITH clause is informational and dropped.
15814        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15815            self.advance();
15816            if !matches!(self.peek(), Token::LParen) {
15817                return Err(self.err(format!(
15818                    "expected '(' after WITH in CREATE INDEX, got {:?}",
15819                    self.peek()
15820                )));
15821            }
15822            self.advance();
15823            loop {
15824                if matches!(self.peek(), Token::RParen) {
15825                    self.advance();
15826                    break;
15827                }
15828                // Drain `key = value` or bare `key` tokens.
15829                let _ = self.advance(); // key
15830                if matches!(self.peek(), Token::Eq) {
15831                    self.advance();
15832                    let _ = self.advance(); // value (int / string / ident)
15833                }
15834                match self.peek() {
15835                    Token::Comma => {
15836                        self.advance();
15837                    }
15838                    Token::RParen => {
15839                        self.advance();
15840                        break;
15841                    }
15842                    other => {
15843                        return Err(self.err(format!(
15844                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
15845                        )));
15846                    }
15847                }
15848            }
15849        }
15850        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
15851        // which sits between the key list and the WHERE clause.
15852        let mut nulls_not_distinct = false;
15853        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15854            let n1 = self.tokens.get(self.pos + 1);
15855            let n2 = self.tokens.get(self.pos + 2);
15856            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
15857                self.advance(); // NULLS
15858                self.advance(); // NOT
15859                self.advance(); // DISTINCT
15860                nulls_not_distinct = true;
15861            } else if matches!(n1, Some(Token::Distinct)) {
15862                self.advance(); // NULLS
15863                self.advance(); // DISTINCT
15864            }
15865        }
15866        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
15867        let partial_predicate = if matches!(self.peek(), Token::Where) {
15868            self.advance();
15869            Some(self.parse_expr(0)?)
15870        } else {
15871            None
15872        };
15873        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
15874        // sense: uniqueness over an ANN structure has no clean
15875        // semantics. Reject early. (BRIN UNIQUE is similarly
15876        // meaningless — block both.)
15877        if is_unique && !matches!(method, IndexMethod::BTree) {
15878            return Err(self.err(alloc::format!(
15879                "UNIQUE is only supported on BTree indexes, got USING {:?}",
15880                method
15881            )));
15882        }
15883        Ok(Statement::CreateIndex(CreateIndexStatement {
15884            concurrently,
15885            name,
15886            key_order,
15887            key_collation,
15888            table,
15889            column,
15890            nulls_not_distinct,
15891            method,
15892            if_not_exists,
15893            included_columns,
15894            partial_predicate,
15895            extra_columns: extra_columns.clone(),
15896            expression,
15897            is_unique,
15898            opclass,
15899        }))
15900    }
15901
15902    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
15903    /// column-level `REFERENCES ...` clause. The trailing FK is
15904    /// normalised into table-level shape (single-element columns +
15905    /// parent_columns) so the engine sees one uniform constraint list.
15906    fn parse_column_def_with_fk(
15907        &mut self,
15908    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
15909        let col = self.parse_column_def()?;
15910        // v7.39 (round 308, V29) — an explicitly named inline FK:
15911        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
15912        // loop leaves this spelling intact precisely so the name can be
15913        // kept here; PG reports it in violation messages and matches it
15914        // in `SET CONSTRAINTS`.
15915        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
15916        {
15917            self.advance();
15918            Some(self.expect_ident_like()?)
15919        } else {
15920            None
15921        };
15922        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
15923        let inline_references = matches!(
15924            self.peek(),
15925            Token::Ident(s) if s.eq_ignore_ascii_case("references")
15926        );
15927        if !inline_references {
15928            return Ok((col, None));
15929        }
15930        let (
15931            parent_table,
15932            parent_columns,
15933            on_delete,
15934            on_update,
15935            match_type,
15936            deferrable,
15937            initially_deferred,
15938        ) = self.parse_references_tail(1)?;
15939        let fk = ForeignKeyConstraint {
15940            name: declared_name,
15941            columns: vec![col.name.clone()],
15942            parent_table,
15943            parent_columns,
15944            on_delete,
15945            on_update,
15946            match_type,
15947            deferrable,
15948            initially_deferred,
15949        };
15950        Ok((col, Some(fk)))
15951    }
15952
15953    /// v7.13.0 — parse a column type (consuming the type ident and
15954    /// any trailing parameters / `[]`), without surrounding column
15955    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
15956    /// Returns the resolved `ColumnTypeName` plus implied
15957    /// `(auto_increment, not_null)` flags from PG SERIAL family
15958    /// shorthands — callers that don't expect those (ALTER COLUMN
15959    /// TYPE) can discard them.
15960    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
15961        let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
15962        Ok(ty)
15963    }
15964
15965    #[allow(clippy::type_complexity)]
15966    fn parse_type_with_implied_flags(
15967        &mut self,
15968    ) -> Result<
15969        (
15970            ColumnTypeName,
15971            bool,
15972            bool,
15973            Option<String>,
15974            Collation,
15975            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
15976            bool,
15977            // v7.39 (round 676) — the collation NAME as written, which the
15978            // `Collation` enum above cannot carry.
15979            Option<String>,
15980            bool,
15981            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
15982            // list captured at type-parse time. None for all
15983            // non-ENUM types.
15984            Option<Vec<String>>,
15985            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
15986            // list. Distinct from ENUM (subset semantics).
15987            Option<Vec<String>>,
15988            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
15989            // width, lost when the type collapses to SmallInt / Int.
15990            Option<MysqlIntWidth>,
15991            // v7.39 (round 424) — declared fractional-seconds precision of a
15992            // MySQL temporal column (bare spelling = 0). None under PG.
15993            Option<u8>,
15994        ),
15995        ParseError,
15996    > {
15997        let mut ty_ident = match self.advance() {
15998            Token::Ident(s) => s,
15999            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16000            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16001            // '<span>'` literal grammar. As a column type it lands
16002            // here directly; downstream resolution still uses the
16003            // canonical lowercase string.
16004            Token::Interval => "interval".to_string(),
16005            other => {
16006                return Err(ParseError {
16007                    message: format!("expected column type, got {other:?}"),
16008                    token_pos: self.consumed_pos(),
16009                });
16010            }
16011        };
16012        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16013        // pg_dump qualifies extension types (`public.vector(1024)`).
16014        // SPG is single-namespace; drop the schema and resolve the
16015        // bare type — same treatment table names already get.
16016        while matches!(self.peek(), Token::Dot) {
16017            self.advance();
16018            ty_ident = self.expect_ident_like()?;
16019        }
16020        let mut implied_auto_increment = false;
16021        let mut implied_not_null = false;
16022        let mut user_type_ref: Option<String> = None;
16023        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16024        // value list, captured here and bubbled up through the
16025        // ColumnDef so the engine can attach it to the column
16026        // schema (and validate INSERT cells against it).
16027        let mut inline_enum_variants: Option<Vec<String>> = None;
16028        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16029        let mut inline_set_variants: Option<Vec<String>> = None;
16030        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16031        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16032        // collapses to SmallInt / Int. Only under the MySQL dialect.
16033        let mut mysql_int_width: Option<MysqlIntWidth> = None;
16034        // v7.39 (round 424) — the declared fractional-seconds precision of a
16035        // MySQL temporal column. Set by the temporal arms below; stays None
16036        // for PG (whose temporal columns keep full microseconds).
16037        let mut mysql_fsp: Option<u8> = None;
16038        let mut ty = match ty_ident.as_str() {
16039            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16040            "smallserial" | "serial2" => {
16041                implied_auto_increment = true;
16042                implied_not_null = true;
16043                ColumnTypeName::SmallInt
16044            }
16045            "serial" | "serial4" => {
16046                implied_auto_increment = true;
16047                implied_not_null = true;
16048                ColumnTypeName::Int
16049            }
16050            "bigserial" | "serial8" => {
16051                implied_auto_increment = true;
16052                implied_not_null = true;
16053                ColumnTypeName::BigInt
16054            }
16055            // MySQL flavours we accept by aliasing to the closest SPG
16056            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16057            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16058            // 24-bit) → INT. UNSIGNED modifiers are consumed below
16059            // without semantic effect.
16060            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16061            // PG's internal type names; pg_dump and hand-written PG schemas
16062            // use them interchangeably with smallint / int / bigint (the cast
16063            // path already accepted them, only the column grammar didn't).
16064            "smallint" | "int2" => {
16065                // v7.14.0 — MySQL display-width on integers
16066                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16067                // parenthesised number is purely cosmetic — it
16068                // doesn't change storage. Accept + discard.
16069                self.consume_optional_paren_size();
16070                ColumnTypeName::SmallInt
16071            }
16072            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16073            // canonical encoding for BOOLEAN. Every MySQL driver
16074            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16075            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16076            // 4.3 SPG classified TINYINT(1) as SmallInt, which
16077            // gave the customer i16-shaped values where the app
16078            // expected bool — a Tier-A silent type drift on
16079            // mysqldump restores. Now: `TINYINT(1)` → Bool;
16080            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16081            // stay SmallInt (the legacy width-agnostic path).
16082            "tinyint" => {
16083                let width = self.peek_optional_paren_size_value();
16084                self.consume_optional_paren_size();
16085                if width == Some(1) {
16086                    ColumnTypeName::Bool
16087                } else {
16088                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16089                    // lost width so the write path can enforce -128..127.
16090                    if self.mysql_dialect {
16091                        mysql_int_width = Some(MysqlIntWidth::Tiny);
16092                    }
16093                    ColumnTypeName::SmallInt
16094                }
16095            }
16096            "mediumint" => {
16097                self.consume_optional_paren_size();
16098                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16099                if self.mysql_dialect {
16100                    mysql_int_width = Some(MysqlIntWidth::Medium);
16101                }
16102                ColumnTypeName::Int
16103            }
16104            "int" | "integer" | "int4" => {
16105                self.consume_optional_paren_size();
16106                ColumnTypeName::Int
16107            }
16108            "bigint" | "int8" => {
16109                self.consume_optional_paren_size();
16110                ColumnTypeName::BigInt
16111            }
16112            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16113            // (mailrs round-5 G6). Consume the optional `PRECISION`
16114            // tail when the type keyword was `double` / `DOUBLE`.
16115            //
16116            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16117            // FLOAT". `FLOAT(p)` picks the width the way PG does:
16118            // p in 1..=24 is real, 25..=53 is double precision, and
16119            // anything else is an error.
16120            "float" | "double" | "real" => {
16121                if ty_ident.eq_ignore_ascii_case("double")
16122                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16123                {
16124                    self.advance();
16125                }
16126                if ty_ident.eq_ignore_ascii_case("real") {
16127                    // v7.39 (round 274) — the two dialects genuinely
16128                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16129                    // synonym for DOUBLE (8-byte). Round 269 made REAL
16130                    // 32-bit globally and thereby narrowed the stored
16131                    // precision of every MySQL REAL column.
16132                    if self.mysql_dialect {
16133                        ColumnTypeName::Float
16134                    } else {
16135                        ColumnTypeName::Real
16136                    }
16137                } else if ty_ident.eq_ignore_ascii_case("float")
16138                    && self.mysql_dialect
16139                    && matches!(self.peek(), Token::LParen)
16140                    && self.peek_paren_has_comma()
16141                {
16142                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16143                    // display form (`FLOAT(10,2)`), which PG has no
16144                    // equivalent of. It was `syntax error at or near ","`,
16145                    // so the whole CREATE failed. The digits are a display
16146                    // hint only; SPG stores the full double.
16147                    self.consume_optional_paren_size();
16148                    ColumnTypeName::Float
16149                } else if ty_ident.eq_ignore_ascii_case("float")
16150                    && matches!(self.peek(), Token::LParen)
16151                {
16152                    // PG words the two bounds differently, and
16153                    // parse_paren_size already rejects a zero.
16154                    let p = self.parse_paren_size("FLOAT")?;
16155                    if p > 53 {
16156                        return Err(self.err(String::from(
16157                            "precision for type float must be less than 54 bits",
16158                        )));
16159                    }
16160                    if p <= 24 {
16161                        ColumnTypeName::Real
16162                    } else {
16163                        ColumnTypeName::Float
16164                    }
16165                } else {
16166                    ColumnTypeName::Float
16167                }
16168            }
16169            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16170            "float4" => ColumnTypeName::Real,
16171            "float8" => ColumnTypeName::Float,
16172            "text" => ColumnTypeName::Text,
16173            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16174            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16175            // real MySQL schema and NONE of them existed: the CREATE
16176            // failed outright with `type "blob" does not exist`, so the
16177            // table was never made. The sizes differ only in MySQL's
16178            // maximum length, which SPG does not cap, so they collapse
16179            // onto TEXT and BYTEA the way the unsized spellings do.
16180            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16181            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16182            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16183            // enforce, consumed so the declaration parses.
16184            "varbinary" | "binary" => {
16185                self.consume_optional_paren_size();
16186                ColumnTypeName::Bytes
16187            }
16188            "name" => ColumnTypeName::Name,
16189            "xid" => ColumnTypeName::Xid,
16190            "oid" => ColumnTypeName::Oid,
16191            "xid8" => ColumnTypeName::Xid8,
16192            "bool" | "boolean" => ColumnTypeName::Bool,
16193            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16194            // an unbounded `character varying`, which the arm below has always
16195            // read as text. Only the short spelling demanded a length, so
16196            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16197            // there is — failed on `VARCHAR type requires (N)` while the long
16198            // spelling of the same thing was accepted. The same asymmetry
16199            // round 613 closed on the CAST side, here on the DDL side.
16200            "varchar" => {
16201                if matches!(self.peek(), Token::LParen) {
16202                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16203                } else {
16204                    ColumnTypeName::Text
16205                }
16206            }
16207            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16208            // `character` below (SQL standard).
16209            "char" => {
16210                if matches!(self.peek(), Token::LParen) {
16211                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16212                } else {
16213                    ColumnTypeName::Char(1)
16214                }
16215            }
16216            // pg_dump's canonical spellings: `character varying(n)` = varchar,
16217            // `character(n)` = char, bare `character` = char(1). Unbounded
16218            // `character varying` maps to text.
16219            "character" => {
16220                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16221                    self.advance();
16222                    if matches!(self.peek(), Token::LParen) {
16223                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16224                    } else {
16225                        ColumnTypeName::Text
16226                    }
16227                } else if matches!(self.peek(), Token::LParen) {
16228                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16229                } else {
16230                    ColumnTypeName::Char(1)
16231                }
16232            }
16233            "vector" => {
16234                let dim = self.parse_paren_size("VECTOR")?;
16235                let encoding = self.parse_optional_vector_encoding()?;
16236                ColumnTypeName::Vector { dim, encoding }
16237            }
16238            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16239            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16240            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16241            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16242            // DECIMAL(10,2))` — how nearly every money column is written,
16243            // in either dialect — was a syntax error and the table was
16244            // never created. `FIXED` is MySQL's alias alone, so it is
16245            // taken only in that dialect.
16246            "numeric" | "decimal" | "dec" => {
16247                let (precision, scale) = self.parse_optional_numeric_params()?;
16248                ColumnTypeName::Numeric(precision, scale)
16249            }
16250            "fixed" if self.mysql_dialect => {
16251                let (precision, scale) = self.parse_optional_numeric_params()?;
16252                ColumnTypeName::Numeric(precision, scale)
16253            }
16254            "date" => ColumnTypeName::Date,
16255            // MySQL's `DATETIME` is the same domain as standard
16256            // `TIMESTAMP` — accept both spellings.
16257            "timestamp" | "datetime" => {
16258                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16259                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16260                // TIME ZONE` clause, so consume it first.
16261                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16262                // (it truncates on write and pads on render), so capture it;
16263                // a bare spelling means precision 0 there. PG stores µs always
16264                // and keeps `None`.
16265                let n = self.take_optional_paren_size();
16266                if self.mysql_dialect {
16267                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16268                }
16269                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16270                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16271                // the full form. SPG canonicalises:
16272                //   - WITH TIME ZONE    → Timestamptz
16273                //   - WITHOUT TIME ZONE → Timestamp
16274                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16275                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16276                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16277                {
16278                    self.advance(); // WITH
16279                    self.advance(); // TIME
16280                    self.advance(); // ZONE
16281                    ColumnTypeName::Timestamptz
16282                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16283                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16284                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16285                {
16286                    self.advance(); // WITHOUT
16287                    self.advance(); // TIME
16288                    self.advance(); // ZONE
16289                    ColumnTypeName::Timestamp
16290                } else {
16291                    // A second `(precision)` cannot legally follow, but the
16292                    // old grammar tolerated it; keep that tolerance.
16293                    self.consume_optional_paren_size();
16294                    ColumnTypeName::Timestamp
16295                }
16296            }
16297            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16298            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16299            // only PG-wire OID differs.
16300            "timestamptz" => {
16301                self.consume_optional_paren_size();
16302                ColumnTypeName::Timestamptz
16303            }
16304            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16305            // validation. We accept the JSONB spelling too because
16306            // most PG clients default to it; SPG doesn't distinguish
16307            // the two (no path-operator perf advantage to model).
16308            "json" => ColumnTypeName::Json,
16309            "jsonb" => ColumnTypeName::Jsonb,
16310            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16311            // surface here. Same storage shape; mapping happens at
16312            // the engine side via the ColumnTypeName → DataType
16313            // resolver. Literal forms are handled at coerce_value
16314            // time so the lexer stays untouched.
16315            "bytea" | "bytes" => ColumnTypeName::Bytes,
16316            // v7.17.0 Phase 7 — PG network address types
16317            // v7.17.0 had a Text-backed fallback here for
16318            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16319            // each to a first-class type; the keywords are
16320            // bound below in the ζ-A block.
16321            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16322            // The actual `to_tsvector` / `@@` / `ts_rank` surface
16323            // arrives in v7.12.1+; the type itself loads here so
16324            // mailrs's `scripts/init-schema.sql` runs unmodified.
16325            "tsvector" => ColumnTypeName::TsVector,
16326            "tsquery" => ColumnTypeName::TsQuery,
16327            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16328            // surface for Django / Rails / Hibernate's default
16329            // PK pattern.
16330            "uuid" => ColumnTypeName::Uuid,
16331            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16332            // Storage = three-field {months, days, micros}, catalog
16333            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16334            // line `INTERVAL` was parser-rejected at CREATE TABLE.
16335            "interval" => {
16336                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16337                // SECOND` and an optional `(p)` precision. SPG stores the full
16338                // {months,days,micros}; consume + ignore the qualifier/precision.
16339                while matches!(self.peek(), Token::To)
16340                    || matches!(self.peek(), Token::Ident(s) if matches!(
16341                        s.to_ascii_lowercase().as_str(),
16342                        "year" | "month" | "day" | "hour" | "minute" | "second"
16343                    ))
16344                {
16345                    self.advance();
16346                }
16347                self.consume_optional_paren_size();
16348                ColumnTypeName::Interval
16349            }
16350            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16351            // i64 microseconds since 00:00:00. Wire OID 1083.
16352            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16353            "time" => {
16354                // v7.39 (round 424) — MySQL TIME carries a semantic
16355                // fractional-seconds precision, bare meaning 0.
16356                let n = self.take_optional_paren_size();
16357                if self.mysql_dialect {
16358                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16359                }
16360                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16361                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16362                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16363                {
16364                    self.advance();
16365                    self.advance();
16366                    self.advance();
16367                    ColumnTypeName::TimeTz
16368                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16369                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16370                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16371                {
16372                    self.advance();
16373                    self.advance();
16374                    self.advance();
16375                    ColumnTypeName::Time
16376                } else {
16377                    ColumnTypeName::Time
16378                }
16379            }
16380            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16381            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16382            "year" => ColumnTypeName::Year,
16383            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16384            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16385            "timetz" => ColumnTypeName::TimeTz,
16386            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16387            // Wire OID 790.
16388            "money" => ColumnTypeName::Money,
16389            // v7.17.0 Phase 3.P0-38 — PG range types.
16390            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16391            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16392            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16393            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16394            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16395            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16396            // v7.37.5 δ — PG 14+ multirange keywords.
16397            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16398            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16399            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16400            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16401            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16402            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16403            // v7.37.5 ε — PG geometry scalar keywords.
16404            "point" => ColumnTypeName::Point,
16405            "lseg" => ColumnTypeName::Lseg,
16406            "path" => ColumnTypeName::Path,
16407            "box" => ColumnTypeName::PgBox,
16408            "polygon" => ColumnTypeName::Polygon,
16409            "line" => ColumnTypeName::Line,
16410            "circle" => ColumnTypeName::Circle,
16411            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16412            "inet" => ColumnTypeName::Inet,
16413            "cidr" => ColumnTypeName::Cidr,
16414            "macaddr" => ColumnTypeName::Macaddr,
16415            "macaddr8" => ColumnTypeName::Macaddr8,
16416            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16417            // width in the value, so the optional `(N)` typmod is accepted and
16418            // ignored (the column stores whatever width it's given).
16419            "bit" => {
16420                let varying = matches!(
16421                    self.peek(),
16422                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16423                );
16424                if varying {
16425                    self.advance();
16426                }
16427                // v7.39 (round 281) — the length used to be parsed and
16428                // dropped, so `bit(3)` accepted a five-bit string.
16429                let n = if matches!(self.peek(), Token::LParen) {
16430                    self.parse_paren_size("BIT")?
16431                } else {
16432                    0
16433                };
16434                if varying {
16435                    ColumnTypeName::BitVarying(n)
16436                } else {
16437                    ColumnTypeName::Bit(n)
16438                }
16439            }
16440            "varbit" => {
16441                let n = if matches!(self.peek(), Token::LParen) {
16442                    self.parse_paren_size("VARBIT")?
16443                } else {
16444                    0
16445                };
16446                ColumnTypeName::BitVarying(n)
16447            }
16448            "xml" => ColumnTypeName::Xml,
16449            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16450            "hstore" => ColumnTypeName::Hstore,
16451            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16452            // `ENUM('a','b','c')`. Storage is TEXT; the value
16453            // list lands on `inline_enum_variants` for the
16454            // engine to validate INSERT cells against. Empty
16455            // value list is a parse error (matches MySQL).
16456            "enum" => {
16457                // Expect the opening `(`.
16458                if !matches!(self.peek(), Token::LParen) {
16459                    return Err(self.err(alloc::format!(
16460                        "expected '(' after ENUM, got {:?}",
16461                        self.peek()
16462                    )));
16463                }
16464                self.advance();
16465                let mut variants: Vec<String> = Vec::new();
16466                loop {
16467                    match self.advance() {
16468                        Token::String(s) => variants.push(s),
16469                        other => {
16470                            return Err(self.err(alloc::format!(
16471                                "ENUM(...) expects string literal variants, got {other:?}"
16472                            )));
16473                        }
16474                    }
16475                    match self.peek() {
16476                        Token::Comma => {
16477                            self.advance();
16478                            continue;
16479                        }
16480                        Token::RParen => {
16481                            self.advance();
16482                            break;
16483                        }
16484                        other => {
16485                            return Err(self.err(alloc::format!(
16486                                "expected ',' or ')' in ENUM(...), got {other:?}"
16487                            )));
16488                        }
16489                    }
16490                }
16491                if variants.is_empty() {
16492                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
16493                }
16494                inline_enum_variants = Some(variants);
16495                // Storage is plain TEXT; the variant list lives on
16496                // the ColumnSchema side.
16497                ColumnTypeName::Text
16498            }
16499            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16500            // `SET('a','b','c')`. Same parse shape as ENUM;
16501            // semantics differ (subset rather than pick-one).
16502            "set" => {
16503                if !matches!(self.peek(), Token::LParen) {
16504                    return Err(self.err(alloc::format!(
16505                        "expected '(' after SET, got {:?}",
16506                        self.peek()
16507                    )));
16508                }
16509                self.advance();
16510                let mut variants: Vec<String> = Vec::new();
16511                loop {
16512                    match self.advance() {
16513                        Token::String(s) => variants.push(s),
16514                        other => {
16515                            return Err(self.err(alloc::format!(
16516                                "SET(...) expects string literal variants, got {other:?}"
16517                            )));
16518                        }
16519                    }
16520                    match self.peek() {
16521                        Token::Comma => {
16522                            self.advance();
16523                            continue;
16524                        }
16525                        Token::RParen => {
16526                            self.advance();
16527                            break;
16528                        }
16529                        other => {
16530                            return Err(self.err(alloc::format!(
16531                                "expected ',' or ')' in SET(...), got {other:?}"
16532                            )));
16533                        }
16534                    }
16535                }
16536                if variants.is_empty() {
16537                    return Err(self.err("SET(...) must declare at least one variant".into()));
16538                }
16539                inline_set_variants = Some(variants);
16540                ColumnTypeName::Text
16541            }
16542            _other => {
16543                // v7.17.0 Phase 1.4 — unknown ident → defer
16544                // resolution to the engine. Stored as Text in
16545                // ColumnTypeName + the original name carried as
16546                // `user_type_ref` so CREATE TABLE can look up
16547                // user-defined enum / domain types.
16548                user_type_ref = Some(ty_ident.clone());
16549                ColumnTypeName::Text
16550            }
16551        };
16552        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16553        // right after the type keyword. Pre-4.4 SPG consumed +
16554        // discarded the keyword, leaving a customer column
16555        // declared `id INT UNSIGNED NOT NULL` silently accepting
16556        // negative values — a Tier-A correctness drift where
16557        // application invariants (auto-increment-IDs never
16558        // negative) silently broke on cutover. Now: capture as
16559        // a column flag, persist on the schema, enforce at
16560        // INSERT / UPDATE time.
16561        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16562        {
16563            self.advance();
16564            true
16565        } else {
16566            false
16567        };
16568        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16569        // `<type> COLLATE <name>` post-fixes on text columns. SPG
16570        // stores text as UTF-8 always so CHARACTER SET is still a
16571        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16572        // name: it gets classified into a `Collation` variant the
16573        // engine consults at WHERE-eval time. PG `default` /
16574        // `pg_catalog.default` / `C` / `POSIX` collations all
16575        // resolve to `Binary` (the prior behaviour); `_ci` /
16576        // `case_insensitive` / `nocase` shift to CaseInsensitive.
16577        // The schema-qualifier form (`pg_catalog.default`) lexes
16578        // as `Ident '.' Ident` — peek for the `.` and consume both
16579        // halves so it's treated as one collation name. PG's
16580        // `IDENT.IDENT` collation form (which can appear here) is
16581        // resolved by Collation::from_collation_name on the bare
16582        // identifier after the dot.
16583        let mut collation = Collation::Binary;
16584        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16585        // clause was written. The engine needs this to tell an explicit
16586        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16587        // clause at all: both resolve to `Collation::Binary`, but under the
16588        // MySQL dialect the latter takes the folding default collation.
16589        let mut collation_explicit = false;
16590        let mut collation_name: Option<alloc::string::String> = None;
16591        loop {
16592            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16593                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16594            {
16595                self.advance(); // CHARACTER
16596                self.advance(); // SET
16597                if matches!(
16598                    self.peek(),
16599                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16600                ) {
16601                    self.advance();
16602                }
16603                continue;
16604            }
16605            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16606                self.advance(); // COLLATE
16607                // Accept Ident / QuotedIdent / String AND the
16608                // keyword-tokenised `Default` (PG `pg_catalog.default`
16609                // and bare `DEFAULT` collation names — `default` is a
16610                // reserved word so the lexer hands back Token::Default
16611                // not Token::Ident).
16612                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16613                    match this.peek().clone() {
16614                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16615                            this.advance();
16616                            Some(s)
16617                        }
16618                        Token::Default => {
16619                            this.advance();
16620                            Some(alloc::string::String::from("default"))
16621                        }
16622                        _ => None,
16623                    }
16624                };
16625                let raw = if let Some(head) = read_collation_atom(self) {
16626                    // Schema-qualified PG form: `pg_catalog.default`.
16627                    if matches!(self.peek(), Token::Dot) {
16628                        self.advance();
16629                        let tail = read_collation_atom(self).unwrap_or_default();
16630                        alloc::format!("{head}.{tail}")
16631                    } else {
16632                        head
16633                    }
16634                } else {
16635                    alloc::string::String::new()
16636                };
16637                if !raw.is_empty() {
16638                    collation_explicit = true;
16639                    // v7.39 (round 676) — keep the name too. The enum below
16640                    // folds C / POSIX / en_US / default into one value, and
16641                    // `pg_attribute.attcollation` has to tell them apart.
16642                    // The schema qualifier goes: PG's `pg_catalog.default`
16643                    // and a bare `default` name the same collation.
16644                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
16645                    // encoding suffix. Round 676 used `rsplit('.')` for
16646                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
16647                    // PG writes `pg_catalog.default` (qualifier) and
16648                    // `en_US.utf8` (locale + encoding) with the same
16649                    // separator. Only `pg_catalog.` is a qualifier, and it
16650                    // is the only one PG's own dumps emit.
16651                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
16652                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
16653                    collation_name = Some(alloc::string::String::from(bare));
16654                    let parsed = Collation::from_collation_name(&raw);
16655                    // Last COLLATE clause wins, but `Binary` from a
16656                    // bare keyword like `default` should not
16657                    // silently downgrade a stronger one set earlier
16658                    // on the same column. v7.17 only ships one
16659                    // non-Binary variant so a simple OR is enough.
16660                    if parsed != Collation::Binary {
16661                        collation = parsed;
16662                    }
16663                }
16664                continue;
16665            }
16666            break;
16667        }
16668        // v7.10.10 — postfix `[]` widens the base type to its array
16669        // type. PG accepts `TYPE[]` after any base type and so does
16670        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
16671        // all through; the old "only TEXT[]" note was stale).
16672        if matches!(self.peek(), Token::LBracket) {
16673            self.advance();
16674            if !matches!(self.peek(), Token::RBracket) {
16675                return Err(self.err(alloc::format!(
16676                    "TEXT[] takes no dimension; got {:?}",
16677                    self.peek()
16678                )));
16679            }
16680            self.advance();
16681            // v7.11.13 — widened to INT[] and BIGINT[] in addition
16682            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
16683            // still error here.
16684            ty = match ty {
16685                ColumnTypeName::Text => ColumnTypeName::TextArray,
16686                ColumnTypeName::Int => ColumnTypeName::IntArray,
16687                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
16688                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
16689                // `[]` grammar. Wire OID 1187.
16690                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
16691                // v7.37.5 γ — full PG array-of-scalar family.
16692                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
16693                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
16694                ColumnTypeName::Float => ColumnTypeName::FloatArray,
16695                // NUMERIC(p, s) loses its precision params at the
16696                // array level (matches PG: `NUMERIC[]` is untyped,
16697                // per-element precision flows through values).
16698                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
16699                ColumnTypeName::Date => ColumnTypeName::DateArray,
16700                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
16701                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
16702                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
16703                ColumnTypeName::Json => ColumnTypeName::JsonArray,
16704                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
16705                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
16706                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
16707                // the array level (matches PG semantics where the
16708                // element precision is per-row, not column-wide).
16709                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
16710                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
16711                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
16712                // follow-up.
16713                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
16714                other => {
16715                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
16716                }
16717            };
16718            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
16719            // for INT/TEXT/BIGINT. Anything else is an error.
16720            if matches!(self.peek(), Token::LBracket) {
16721                self.advance();
16722                if !matches!(self.peek(), Token::RBracket) {
16723                    return Err(self.err(alloc::format!(
16724                        "TYPE[][] second dimension takes no size; got {:?}",
16725                        self.peek()
16726                    )));
16727                }
16728                self.advance();
16729                ty = match ty {
16730                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
16731                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
16732                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
16733                    // v7.39 (read01 round 75) — bool[][].
16734                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
16735                    other => {
16736                        return Err(self.err(alloc::format!(
16737                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
16738                             TEXT[][] only; got {other:?}"
16739                        )));
16740                    }
16741                };
16742            }
16743        }
16744        Ok((
16745            ty,
16746            implied_auto_increment,
16747            implied_not_null,
16748            user_type_ref,
16749            collation,
16750            collation_explicit,
16751            collation_name,
16752            is_unsigned,
16753            inline_enum_variants,
16754            inline_set_variants,
16755            mysql_int_width,
16756            mysql_fsp,
16757        ))
16758    }
16759
16760    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
16761        // v7.20 — PG reserves the table-constraint keywords, so a
16762        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
16763        // malformed constraint clause (e.g. `UNIQUE a` missing its
16764        // parens), not a column named "unique". Since v7.17's
16765        // unknown-type leniency (`user_type_ref`) such a clause
16766        // would otherwise parse as a column with a user-defined
16767        // type — silently accepting invalid DDL. Quoted
16768        // identifiers ("unique" / `unique`) remain valid names.
16769        if let Token::Ident(s) = self.peek()
16770            && [
16771                "unique",
16772                "primary",
16773                "foreign",
16774                "constraint",
16775                "check",
16776                "references",
16777                "exclude",
16778            ]
16779            .iter()
16780            .any(|kw| s.eq_ignore_ascii_case(kw))
16781        {
16782            return Err(self.err(alloc::format!(
16783                "unexpected reserved keyword '{s}' at start of column definition \
16784                 (malformed table constraint?)"
16785            )));
16786        }
16787        let name = self.expect_ident_like()?;
16788        let (
16789            ty,
16790            implied_auto_increment,
16791            implied_not_null,
16792            user_type_ref,
16793            collation,
16794            collation_explicit,
16795            collation_name,
16796            is_unsigned,
16797            inline_enum_variants,
16798            inline_set_variants,
16799            mysql_int_width,
16800            mysql_fsp,
16801        ) = self.parse_type_with_implied_flags()?;
16802        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
16803        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
16804        // each at most once.
16805        let mut default: Option<Expr> = None;
16806        let mut nullable = !implied_not_null;
16807        let mut nullability_seen = implied_not_null;
16808        let mut auto_increment = implied_auto_increment;
16809        let mut is_primary_key = false;
16810        let mut is_unique = false;
16811        let mut unique_nulls_not_distinct = false;
16812        let mut constraint_deferrable = false;
16813        let mut constraint_initially_deferred = false;
16814        let mut check: Option<Expr> = None;
16815        let mut on_update_runtime: Option<Expr> = None;
16816        let mut generated_stored_expr: Option<Box<Expr>> = None;
16817        let mut identity_always = false;
16818        loop {
16819            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
16820            // not-null constraints by name and pg_dump emits them
16821            // inline: `id bigint CONSTRAINT contacts_id_not_null1
16822            // NOT NULL`. Accept and discard the name; whatever
16823            // constraint follows is parsed by the arms below.
16824            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16825                // v7.39 (round 308, V29) — a name on an inline
16826                // REFERENCES belongs to the FOREIGN KEY, and the caller
16827                // (`parse_column_def_with_fk`) is what builds it, so
16828                // leave the whole clause for it. Dropping the name here
16829                // is what made `CONSTRAINT fk_a REFERENCES …` come back
16830                // as the synthesised `c_pid_fkey` — which then could
16831                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
16832                // `advance()` takes tokens by `mem::replace`, so there
16833                // is no rewinding once consumed.
16834                if matches!(
16835                    self.tokens.get(self.pos + 2),
16836                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
16837                ) {
16838                    break;
16839                }
16840                self.advance();
16841                let _name = self.expect_ident_like()?;
16842                continue;
16843            }
16844            // v7.39 (round 379) — MySQL's SHORT generated-column form
16845            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
16846            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
16847            // below), but hand-written schemas and app migrations use this.
16848            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
16849            // SPG computes-and-stores either way, like the long form.
16850            if matches!(self.peek(), Token::As) {
16851                self.advance();
16852                if !matches!(self.peek(), Token::LParen) {
16853                    return Err(self.err(alloc::format!(
16854                        "expected '(' after AS in a generated column, got {:?}",
16855                        self.peek()
16856                    )));
16857                }
16858                self.advance();
16859                let expr = self.parse_expr(0)?;
16860                if !matches!(self.peek(), Token::RParen) {
16861                    return Err(self.err(alloc::format!(
16862                        "expected ')' after AS (<expr>), got {:?}",
16863                        self.peek()
16864                    )));
16865                }
16866                self.advance();
16867                if matches!(self.peek(), Token::Ident(s)
16868                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
16869                {
16870                    self.advance();
16871                }
16872                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
16873                continue;
16874            }
16875            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
16876            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
16877            // the modern replacement for SERIAL in hand-written
16878            // schemas). Both flavours map onto the auto-increment
16879            // machinery — SPG's serial semantics ≈ BY DEFAULT;
16880            // ALWAYS's reject-explicit-values nuance is documented
16881            // leniency. Generated EXPRESSION columns
16882            // (`AS (expr) STORED`) are not supported: error loudly
16883            // instead of silently storing NULLs.
16884            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
16885                self.advance();
16886                let mut saw_generated_always = false;
16887                match self.peek().clone() {
16888                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
16889                        self.advance();
16890                        saw_generated_always = true;
16891                    }
16892                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
16893                        self.advance();
16894                        if !matches!(self.peek(), Token::Default) {
16895                            return Err(self.err(alloc::format!(
16896                                "expected DEFAULT after GENERATED BY, got {:?}",
16897                                self.peek()
16898                            )));
16899                        }
16900                        self.advance();
16901                    }
16902                    other => {
16903                        return Err(self.err(alloc::format!(
16904                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
16905                        )));
16906                    }
16907                }
16908                if !matches!(self.peek(), Token::As) {
16909                    return Err(self.err(alloc::format!(
16910                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
16911                        self.peek()
16912                    )));
16913                }
16914                self.advance();
16915                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
16916                // ( <expr> ) STORED` stored computed-column. The
16917                // expression is captured for the engine to recompute
16918                // on every INSERT / UPDATE. v7.37.7 accepts the
16919                // STORED keyword only; PG also has VIRTUAL, which
16920                // v7.37.7 carves out (sentori only uses STORED).
16921                if matches!(self.peek(), Token::LParen) {
16922                    self.advance();
16923                    let expr = self.parse_expr(0)?;
16924                    if !matches!(self.peek(), Token::RParen) {
16925                        return Err(self.err(alloc::format!(
16926                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
16927                            self.peek()
16928                        )));
16929                    }
16930                    self.advance();
16931                    let stored = match self.peek() {
16932                        Token::Ident(s) | Token::QuotedIdent(s)
16933                            if s.eq_ignore_ascii_case("stored") =>
16934                        {
16935                            self.advance();
16936                            true
16937                        }
16938                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
16939                        // generated columns. SPG computes them on write and
16940                        // persists like STORED; the two are observably
16941                        // identical for query results (the value, recompute
16942                        // on base-column change, and NOT NULL enforcement all
16943                        // match), so a PG 18 schema/dump using VIRTUAL loads
16944                        // and behaves correctly. The compute-on-read storage
16945                        // saving is an invisible internal difference.
16946                        Token::Ident(s) | Token::QuotedIdent(s)
16947                            if s.eq_ignore_ascii_case("virtual") =>
16948                        {
16949                            self.advance();
16950                            false
16951                        }
16952                        other => {
16953                            return Err(self.err(alloc::format!(
16954                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
16955                                 got {other:?}"
16956                            )));
16957                        }
16958                    };
16959                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
16960                    generated_stored_expr = Some(Box::new(expr));
16961                    continue;
16962                }
16963                self.expect_keyword_ident("identity")?;
16964                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
16965                // consume the balanced parens and discard (SPG's
16966                // auto-increment is max+1-scan based).
16967                if matches!(self.peek(), Token::LParen) {
16968                    let mut depth = 0usize;
16969                    loop {
16970                        match self.advance() {
16971                            Token::LParen => depth += 1,
16972                            Token::RParen => {
16973                                depth -= 1;
16974                                if depth == 0 {
16975                                    break;
16976                                }
16977                            }
16978                            Token::Eof => {
16979                                return Err(self.err(
16980                                    "unterminated sequence-options parens after IDENTITY".into(),
16981                                ));
16982                            }
16983                            _ => {}
16984                        }
16985                    }
16986                }
16987                auto_increment = true;
16988                // v7.38 (read01) — remember the ALWAYS flavour so the engine
16989                // can reject explicit non-DEFAULT INSERT values (unless
16990                // OVERRIDING SYSTEM VALUE) the way PG does.
16991                identity_always = saw_generated_always;
16992                // PG identity columns are implicitly NOT NULL.
16993                nullable = false;
16994                continue;
16995            }
16996            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
16997            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
16998            // is accepted today. The "ON" token is an Ident
16999            // (not reserved) — peek before consuming.
17000            if matches!(self.peek(), Token::On)
17001                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17002            {
17003                self.advance(); // ON
17004                self.advance(); // update
17005                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17006                let next = self.peek().clone();
17007                match next {
17008                    Token::Ident(s) | Token::QuotedIdent(s)
17009                        if s.eq_ignore_ascii_case("current_timestamp") =>
17010                    {
17011                        self.advance();
17012                        // Optional `(N)` precision.
17013                        if matches!(self.peek(), Token::LParen) {
17014                            self.advance();
17015                            if !matches!(self.peek(), Token::Integer(_)) {
17016                                return Err(self.err(alloc::format!(
17017                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17018                                    self.peek()
17019                                )));
17020                            }
17021                            self.advance();
17022                            if !matches!(self.peek(), Token::RParen) {
17023                                return Err(self.err(alloc::format!(
17024                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17025                                    self.peek()
17026                                )));
17027                            }
17028                            self.advance();
17029                        }
17030                        on_update_runtime = Some(Expr::FunctionCall {
17031                            name: "now".into(),
17032                            args: Vec::new(),
17033                        });
17034                        continue;
17035                    }
17036                    other => {
17037                        return Err(self.err(alloc::format!(
17038                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17039                        )));
17040                    }
17041                }
17042            }
17043            if matches!(self.peek(), Token::Default) {
17044                if default.is_some() {
17045                    return Err(self.err("DEFAULT specified twice".into()));
17046                }
17047                self.advance();
17048                default = Some(self.parse_expr(0)?);
17049                continue;
17050            }
17051            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17052            // token with NOT NULL and sits EARLIER in the loop than the
17053            // deferrability arm, so without the lookahead it was reported as
17054            // "NOT NULL specified twice" (or "expected NULL after NOT").
17055            if matches!(self.peek(), Token::Not)
17056                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17057            {
17058                // NOT DEFERRABLE — explicit immediate; nothing to carry.
17059                self.consume_optional_deferrable_clauses()?;
17060                continue;
17061            }
17062            if matches!(self.peek(), Token::Not) {
17063                if nullability_seen {
17064                    return Err(self.err("NOT NULL specified twice".into()));
17065                }
17066                self.advance();
17067                if !matches!(self.peek(), Token::Null) {
17068                    return Err(self.err(format!(
17069                        "expected NULL after NOT in column def, got {:?}",
17070                        self.peek()
17071                    )));
17072                }
17073                self.advance();
17074                nullable = false;
17075                nullability_seen = true;
17076                continue;
17077            }
17078            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17079            // "this column is nullable" marker (the default in
17080            // standard SQL anyway). mysqldump emits it routinely
17081            // (`col TYPE NULL DEFAULT NULL` for nullable
17082            // timestamps etc). Accept + no-op.
17083            if matches!(self.peek(), Token::Null) {
17084                if nullability_seen && !nullable {
17085                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
17086                    // sentence, PG18-measured (the table name is the
17087                    // caller's; the column half is exact).
17088                    return Err(self.err(alloc::format!(
17089                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
17090                    )));
17091                }
17092                self.advance();
17093                nullable = true;
17094                nullability_seen = true;
17095                continue;
17096            }
17097            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17098            // arrives as a bare Ident. Match either, case-insensitive.
17099            if let Token::Ident(s) = self.peek()
17100                && (s.eq_ignore_ascii_case("auto_increment")
17101                    || s.eq_ignore_ascii_case("autoincrement"))
17102            {
17103                if auto_increment {
17104                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
17105                }
17106                self.advance();
17107                auto_increment = true;
17108                continue;
17109            }
17110            // v7.9.13 — inline `PRIMARY KEY` column constraint
17111            // (mailrs F1). Implies `NOT NULL`. The engine creates
17112            // a BTree index for the PK column at CREATE TABLE time
17113            // so FK parent-side index lookups resolve.
17114            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17115            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17116            // spelling was a parse error, so a pg_dump carrying one stopped
17117            // mid-restore. The clauses are consumed by the same helper the FK
17118            // path has used since round 288 and recorded nowhere: SPG enforces
17119            // the constraint IMMEDIATELY either way, which fails earlier than
17120            // PG inside a transaction that violates-then-repairs — a refusal,
17121            // not a wrong answer. True deferral is the open remainder of F08.
17122            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17123                || (matches!(self.peek(), Token::Not)
17124                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17125            {
17126                // v7.39 (round 711) — CARRIED now (the storing half of
17127                // F08); round 621 only consumed.
17128                let (d, idef) = self.consume_deferrable_clauses_timed()?;
17129                constraint_deferrable |= d;
17130                constraint_initially_deferred |= idef;
17131                continue;
17132            }
17133            if let Token::Ident(s) = self.peek()
17134                && s.eq_ignore_ascii_case("primary")
17135            {
17136                if is_primary_key {
17137                    return Err(self.err("PRIMARY KEY specified twice".into()));
17138                }
17139                // Peek-ahead for the required `KEY` token.
17140                let next = self.tokens.get(self.pos + 1);
17141                let next_is_key = matches!(
17142                    next,
17143                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17144                );
17145                if !next_is_key {
17146                    return Err(self.err(format!(
17147                        "expected KEY after PRIMARY in column def, got {:?}",
17148                        next
17149                    )));
17150                }
17151                self.advance(); // PRIMARY
17152                self.advance(); // KEY
17153                is_primary_key = true;
17154                if nullability_seen && nullable {
17155                    return Err(self.err(
17156                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17157                    ));
17158                }
17159                nullable = false;
17160                nullability_seen = true;
17161                continue;
17162            }
17163            // v7.13.0 — inline `UNIQUE` column constraint
17164            // (mailrs round-5 G2). Fold into a single-column
17165            // table-level UNIQUE at CREATE TABLE post-process time.
17166            if let Token::Ident(s) = self.peek()
17167                && s.eq_ignore_ascii_case("unique")
17168            {
17169                if is_unique {
17170                    return Err(self.err("UNIQUE specified twice".into()));
17171                }
17172                self.advance();
17173                is_unique = true;
17174                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17175                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17176                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17177                    let n1 = self.tokens.get(self.pos + 1);
17178                    let n2 = self.tokens.get(self.pos + 2);
17179                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17180                        self.advance(); // NULLS
17181                        self.advance(); // NOT
17182                        self.advance(); // DISTINCT
17183                        unique_nulls_not_distinct = true;
17184                    } else if matches!(n1, Some(Token::Distinct)) {
17185                        self.advance(); // NULLS
17186                        self.advance(); // DISTINCT
17187                    }
17188                }
17189                continue;
17190            }
17191            // v7.13.0 — inline `CHECK (<expr>)` column constraint
17192            // (mailrs round-5 G3). PG semantics: column-level
17193            // CHECK is equivalent to a table-level CHECK. Multiple
17194            // inline CHECKs on the same column AND together.
17195            if let Token::Ident(s) = self.peek()
17196                && s.eq_ignore_ascii_case("check")
17197            {
17198                self.advance();
17199                if !matches!(self.peek(), Token::LParen) {
17200                    return Err(self.err(alloc::format!(
17201                        "expected '(' after CHECK in column def, got {:?}",
17202                        self.peek()
17203                    )));
17204                }
17205                self.advance();
17206                let pred = self.parse_expr(0)?;
17207                if !matches!(self.peek(), Token::RParen) {
17208                    return Err(self.err(alloc::format!(
17209                        "expected ')' to close CHECK predicate, got {:?}",
17210                        self.peek()
17211                    )));
17212                }
17213                self.advance();
17214                check = Some(match check.take() {
17215                    Some(prev) => Expr::Binary {
17216                        op: BinOp::And,
17217                        lhs: Box::new(prev),
17218                        rhs: Box::new(pred),
17219                    },
17220                    None => pred,
17221                });
17222                continue;
17223            }
17224            break;
17225        }
17226        Ok(ColumnDef {
17227            name,
17228            ty,
17229            nullable,
17230            default,
17231            auto_increment,
17232            is_primary_key,
17233            is_unique,
17234            unique_nulls_not_distinct,
17235            constraint_deferrable,
17236            constraint_initially_deferred,
17237            check,
17238            user_type_ref,
17239            on_update_runtime,
17240            collation,
17241            collation_explicit,
17242            collation_name,
17243            is_unsigned,
17244            inline_enum_variants,
17245            inline_set_variants,
17246            generated_stored_expr,
17247            identity_always,
17248            mysql_int_width,
17249            mysql_fsp,
17250        })
17251    }
17252
17253    /// `NUMERIC` may appear without parameters, with one (precision
17254    /// only, scale=0), or with both. Returns `(precision, scale)` with
17255    /// 0 = unspecified for the bare form.
17256    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17257        if !matches!(self.peek(), Token::LParen) {
17258            // Bare `NUMERIC` — PG treats this as "unlimited precision";
17259            // we surface it as precision=0 to mean "unconstrained" so
17260            // the engine doesn't need a separate variant.
17261            return Ok((0, 0));
17262        }
17263        self.advance();
17264        // v7.39 (round 272) — PG's declared precision runs to 1000, and
17265        // it words the out-of-range case with the value it saw. SPG
17266        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17267        // accepts failed to parse at all; values wider than i128 are
17268        // carried by the arbitrary-precision form.
17269        let precision = match self.advance() {
17270            Token::Integer(n) if (1..=1000).contains(&n) => {
17271                u16::try_from(n).expect("range-checked")
17272            }
17273            Token::Integer(n) => {
17274                return Err(ParseError {
17275                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17276                    token_pos: self.consumed_pos(),
17277                });
17278            }
17279            other => {
17280                return Err(ParseError {
17281                    message: format!(
17282                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17283                    ),
17284                    token_pos: self.consumed_pos(),
17285                });
17286            }
17287        };
17288        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17289        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17290        // then overflows). A negative scale rounds to tens / hundreds / …
17291        let scale = if matches!(self.peek(), Token::Comma) {
17292            self.advance();
17293            let neg = if matches!(self.peek(), Token::Minus) {
17294                self.advance();
17295                true
17296            } else {
17297                false
17298            };
17299            match self.advance() {
17300                Token::Integer(n) => {
17301                    let signed = if neg { -n } else { n };
17302                    if !(-1000..=1000).contains(&signed) {
17303                        return Err(ParseError {
17304                            message: format!(
17305                                "NUMERIC scale {signed} must be between -1000 and 1000"
17306                            ),
17307                            token_pos: self.consumed_pos(),
17308                        });
17309                    }
17310                    i16::try_from(signed).expect("range-checked")
17311                }
17312                other => {
17313                    return Err(ParseError {
17314                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
17315                        token_pos: self.consumed_pos(),
17316                    });
17317                }
17318            }
17319        } else {
17320            0
17321        };
17322        if !matches!(self.peek(), Token::RParen) {
17323            return Err(self.err(format!(
17324                "expected ')' to close NUMERIC params, got {:?}",
17325                self.peek()
17326            )));
17327        }
17328        self.advance();
17329        Ok((precision, scale))
17330    }
17331
17332    /// Parse `(N)` where `N` is a positive integer literal — used by the
17333    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17334    /// for the error message.
17335    /// v6.0.1: parse the optional `USING <encoding>` clause that
17336    /// follows `VECTOR(N)` in a column definition. Missing clause
17337    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17338    /// ident → `ParseError` listing the encodings recognised today.
17339    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17340        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17341            return Ok(VecEncoding::F32);
17342        }
17343        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17344        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17345        // consume the token when the very next token is a known
17346        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17347        // `USING` for the caller — it's the rewrite-expression form.
17348        let n1 = self.tokens.get(self.pos + 1);
17349        let next_is_encoding = matches!(
17350            n1,
17351            Some(Token::Ident(s))
17352                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17353        );
17354        if !next_is_encoding {
17355            return Ok(VecEncoding::F32);
17356        }
17357        self.advance();
17358        let enc_ident = match self.advance() {
17359            Token::Ident(s) => s,
17360            other => {
17361                return Err(self.err(format!(
17362                    "expected vector encoding after USING, got {other:?}"
17363                )));
17364            }
17365        };
17366        match enc_ident.to_ascii_lowercase().as_str() {
17367            "sq8" => Ok(VecEncoding::Sq8),
17368            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17369            // binary16 per-element storage.
17370            "half" => Ok(VecEncoding::F16),
17371            other => Err(self.err(format!(
17372                "unknown vector encoding {other:?}; supported: SQ8, HALF"
17373            ))),
17374        }
17375    }
17376
17377    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17378    /// without consuming it. Returns `Some(N)` when the next
17379    /// tokens are `( <int> )`; None otherwise. Used by the
17380    /// TINYINT classifier to decide whether to map to Bool or
17381    /// SmallInt.
17382    fn peek_optional_paren_size_value(&self) -> Option<i64> {
17383        if !matches!(self.peek(), Token::LParen) {
17384            return None;
17385        }
17386        let next = self.tokens.get(self.pos + 1)?;
17387        let n = match next {
17388            Token::Integer(n) => *n,
17389            _ => return None,
17390        };
17391        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17392            return None;
17393        }
17394        Some(n)
17395    }
17396
17397    /// v7.14.0 — consume an optional MySQL display-width
17398    /// parenthesised number after an integer type, returning
17399    /// nothing. `TINYINT(1)` etc.
17400    /// v7.39 (round 360) — does the parenthesised group ahead contain a
17401    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17402    fn peek_paren_has_comma(&self) -> bool {
17403        let mut i = self.pos + 1;
17404        let mut depth = 1usize;
17405        while depth > 0 {
17406            match self.tokens.get(i) {
17407                Some(Token::LParen) => depth += 1,
17408                Some(Token::RParen) => depth -= 1,
17409                Some(Token::Comma) if depth == 1 => return true,
17410                None | Some(Token::Eof) => return false,
17411                _ => {}
17412            }
17413            i += 1;
17414        }
17415        false
17416    }
17417
17418    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17419    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17420    /// fractional-seconds precision that drives write truncation and render
17421    /// padding, where `consume_optional_paren_size` throws it away.
17422    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17423    fn take_optional_paren_size(&mut self) -> Option<u8> {
17424        let Some(Token::Integer(n)) = self
17425            .tokens
17426            .get(self.pos + 1)
17427            .filter(|_| matches!(self.peek(), Token::LParen))
17428            .cloned()
17429        else {
17430            self.consume_optional_paren_size();
17431            return None;
17432        };
17433        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17434            self.consume_optional_paren_size();
17435            return None;
17436        }
17437        self.consume_optional_paren_size();
17438        u8::try_from(n).ok()
17439    }
17440
17441    fn consume_optional_paren_size(&mut self) {
17442        if !matches!(self.peek(), Token::LParen) {
17443            return;
17444        }
17445        self.advance();
17446        // Skip until matching RParen (allow nested or any tokens).
17447        let mut depth = 1usize;
17448        while depth > 0 {
17449            match self.peek() {
17450                Token::LParen => depth += 1,
17451                Token::RParen => depth -= 1,
17452                Token::Eof => return,
17453                _ => {}
17454            }
17455            self.advance();
17456        }
17457    }
17458
17459    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17460        if !matches!(self.peek(), Token::LParen) {
17461            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17462        }
17463        self.advance();
17464        let n = match self.advance() {
17465            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17466                message: format!("{label} size too large: {n}"),
17467                token_pos: self.consumed_pos(),
17468            })?,
17469            other => {
17470                return Err(ParseError {
17471                    message: format!("expected positive integer {label} size, got {other:?}"),
17472                    token_pos: self.consumed_pos(),
17473                });
17474            }
17475        };
17476        if !matches!(self.peek(), Token::RParen) {
17477            return Err(self.err(format!(
17478                "expected ')' after {label} size, got {:?}",
17479                self.peek()
17480            )));
17481        }
17482        self.advance();
17483        Ok(n)
17484    }
17485
17486    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17487    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17488    /// key, like MySQL) whose action skips conflicting rows.
17489    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17490    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17491    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17492    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17493    /// common bulk-upsert spellings —
17494    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17495    ///     REPLACE INTO t SELECT …
17496    /// — were a parse error / a duplicate-key failure respectively.
17497    ///
17498    /// Precedence: an explicitly written clause beats a statement-level flag.
17499    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17500    /// implicit `REPLACE` and `IGNORE` lowerings.
17501    fn parse_insert_conflict_clause(
17502        &mut self,
17503        replace: bool,
17504        ignore: bool,
17505    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17506        if let Some(c) = self.parse_optional_on_duplicate_key()? {
17507            return Ok(Some(c));
17508        }
17509        if let Some(c) = self.parse_optional_on_conflict()? {
17510            return Ok(Some(c));
17511        }
17512        if replace {
17513            // REPLACE INTO = delete-then-insert, which PG spells as
17514            // `ON CONFLICT DO UPDATE SET` over every column; the engine
17515            // reads an empty assignment list as "take the incoming row".
17516            return Ok(Some(crate::ast::OnConflictClause {
17517                target_columns: Vec::new(),
17518                index_where: None,
17519                constraint_name: None,
17520                mysql_lowered: true,
17521                action: crate::ast::OnConflictAction::Update {
17522                    assignments: Vec::new(),
17523                    where_: None,
17524                },
17525            }));
17526        }
17527        if ignore {
17528            return Ok(Some(Self::insert_ignore_clause()));
17529        }
17530        Ok(None)
17531    }
17532
17533    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17534    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17535    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17536    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17537    fn parse_optional_on_duplicate_key(
17538        &mut self,
17539    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17540        if !(matches!(self.peek(), Token::On)
17541            && matches!(self.tokens.get(self.pos + 1),
17542                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17543        {
17544            return Ok(None);
17545        }
17546        self.advance(); // ON
17547        self.advance(); // DUPLICATE
17548        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17549            return Err(self.err(format!(
17550                "expected KEY after ON DUPLICATE, got {:?}",
17551                self.peek()
17552            )));
17553        }
17554        self.advance();
17555        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17556            return Err(self.err(format!(
17557                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17558                self.peek()
17559            )));
17560        }
17561        self.advance();
17562        let mut assignments: Vec<(String, Expr)> = Vec::new();
17563        loop {
17564            let col = self.expect_ident_like()?;
17565            if !matches!(self.peek(), Token::Eq) {
17566                return Err(self.err(format!(
17567                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17568                    self.peek()
17569                )));
17570            }
17571            self.advance();
17572            let mut expr = self.parse_expr(0)?;
17573            Self::rewrite_mysql_values_refs(&mut expr);
17574            assignments.push((col, expr));
17575            if matches!(self.peek(), Token::Comma) {
17576                self.advance();
17577                continue;
17578            }
17579            break;
17580        }
17581        Ok(Some(crate::ast::OnConflictClause {
17582            target_columns: Vec::new(),
17583            index_where: None,
17584            constraint_name: None,
17585            mysql_lowered: true,
17586            action: crate::ast::OnConflictAction::Update {
17587                assignments,
17588                where_: None,
17589            },
17590        }))
17591    }
17592
17593    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17594        crate::ast::OnConflictClause {
17595            target_columns: Vec::new(),
17596            index_where: None,
17597            constraint_name: None,
17598            mysql_lowered: true,
17599            action: crate::ast::OnConflictAction::Nothing,
17600        }
17601    }
17602
17603    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17604        debug_assert!(
17605            matches!(self.peek(), Token::Insert)
17606                || (replace
17607                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17608        );
17609        self.advance();
17610        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17611        // would raise a duplicate-key error instead of failing the statement,
17612        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17613        // plain ident to the lexer; only the MySQL dialect accepts it here.
17614        let ignore = self.mysql_dialect
17615            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17616        if ignore {
17617            self.advance();
17618        }
17619        if !matches!(self.peek(), Token::Into) {
17620            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17621        }
17622        self.advance();
17623        let table = self.expect_ident_like()?;
17624        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17625        // grammar requires the AS keyword here (a bare identifier would be
17626        // ambiguous with a column list). The alias is what the ON CONFLICT
17627        // DO UPDATE expressions refer to the target row by.
17628        let alias = if matches!(self.peek(), Token::As) {
17629            self.advance();
17630            Some(self.expect_ident_like()?)
17631        } else {
17632            None
17633        };
17634        // v7.39 (round 428) — MySQL's SET-form INSERT:
17635        //     INSERT INTO t SET a = 1, b = 'x'
17636        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
17637        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
17638        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
17639        // measured). So it lowers to the column list + one VALUES row and
17640        // rejoins the ordinary path, which already handles every one of
17641        // those. PG has no such spelling, hence the dialect gate.
17642        if self.mysql_dialect
17643            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
17644        {
17645            self.advance(); // SET
17646            let mut names = Vec::new();
17647            let mut values = Vec::new();
17648            loop {
17649                names.push(self.expect_ident_like()?);
17650                if !matches!(self.peek(), Token::Eq) {
17651                    return Err(self.err(alloc::format!(
17652                        "expected '=' in INSERT … SET, got {:?}",
17653                        self.peek()
17654                    )));
17655                }
17656                self.advance();
17657                // `SET a = DEFAULT` rides the same `__column_default` marker
17658                // the VALUES-row and UPDATE-SET paths use; the INSERT
17659                // executor resolves it against the target column.
17660                if matches!(self.peek(), Token::Default) {
17661                    self.advance();
17662                    values.push(Expr::FunctionCall {
17663                        name: "__column_default".to_string(),
17664                        args: Vec::new(),
17665                    });
17666                } else {
17667                    values.push(self.parse_expr(0)?);
17668                }
17669                if matches!(self.peek(), Token::Comma) {
17670                    self.advance();
17671                    continue;
17672                }
17673                break;
17674            }
17675            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17676            let returning = self.parse_optional_returning()?;
17677            return Ok(Statement::Insert(InsertStatement {
17678                ctes: Vec::new(),
17679                table,
17680                alias,
17681                columns: Some(names),
17682                rows: alloc::vec![values],
17683                select_source: None,
17684                // MySQL's SET form has no `OVERRIDING …` clause (that is
17685                // PG's identity-column spelling).
17686                overriding: Overriding::None,
17687                mysql_ignore: ignore,
17688                on_conflict,
17689                returning,
17690            }));
17691        }
17692        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
17693        // v7.39 (round 151) — a SELECT or WITH right after the paren is
17694        // a parenthesized query source instead (PG select_with_parens:
17695        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
17696        // both keywords are reserved in PG, so no column list can start
17697        // with them.
17698        let columns = if matches!(self.peek(), Token::LParen) {
17699            self.advance();
17700            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17701                let select_stmt = if self.peek_is_with_kw() {
17702                    self.advance();
17703                    self.parse_nested_with_select()?
17704                } else {
17705                    match self.parse_select_stmt()? {
17706                        Statement::Select(s) => s,
17707                        other => {
17708                            return Err(self.err(alloc::format!(
17709                                "expected SELECT in parenthesized INSERT source, got {other:?}"
17710                            )));
17711                        }
17712                    }
17713                };
17714                if !matches!(self.peek(), Token::RParen) {
17715                    return Err(self.err(format!(
17716                        "expected ')' after parenthesized INSERT source, got {:?}",
17717                        self.peek()
17718                    )));
17719                }
17720                self.advance();
17721                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17722                let returning = self.parse_optional_returning()?;
17723                return Ok(Statement::Insert(InsertStatement {
17724                    ctes: Vec::new(),
17725                    table,
17726                    alias: alias.clone(),
17727                    columns: None,
17728                    rows: Vec::new(),
17729                    select_source: Some(Box::new(select_stmt)),
17730                    on_conflict,
17731                    returning,
17732                    overriding: Overriding::None,
17733                    mysql_ignore: ignore,
17734                }));
17735            }
17736            let mut names = Vec::new();
17737            loop {
17738                names.push(self.expect_ident_like()?);
17739                match self.peek() {
17740                    Token::Comma => {
17741                        self.advance();
17742                    }
17743                    Token::RParen => {
17744                        self.advance();
17745                        break;
17746                    }
17747                    other => {
17748                        return Err(self.err(format!(
17749                            "expected ',' or ')' in INSERT column list, got {other:?}"
17750                        )));
17751                    }
17752                }
17753            }
17754            Some(names)
17755        } else {
17756            None
17757        };
17758        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
17759        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
17760        // is captured on the statement so the engine can apply PG's
17761        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
17762        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
17763        {
17764            self.advance();
17765            let which = self.expect_ident_like()?;
17766            let ov = if which.eq_ignore_ascii_case("system") {
17767                Overriding::System
17768            } else if which.eq_ignore_ascii_case("user") {
17769                Overriding::User
17770            } else {
17771                return Err(self.err(format!(
17772                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
17773                )));
17774            };
17775            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
17776                return Err(self.err(format!(
17777                    "expected VALUE after OVERRIDING {}, got {:?}",
17778                    which.to_ascii_uppercase(),
17779                    self.peek()
17780                )));
17781            }
17782            self.advance();
17783            ov
17784        } else {
17785            Overriding::None
17786        };
17787        // `INSERT INTO t DEFAULT VALUES` — a single row made
17788        // entirely of column defaults. Lower to the permuted
17789        // column-list path with an empty list: every schema column
17790        // is unmapped, so the engine fills each from its default
17791        // (serials advance, plain defaults evaluate, the rest NULL).
17792        if matches!(self.peek(), Token::Default) {
17793            self.advance();
17794            if !matches!(self.peek(), Token::Values) {
17795                return Err(self.err(format!(
17796                    "expected VALUES after DEFAULT in INSERT, got {:?}",
17797                    self.peek()
17798                )));
17799            }
17800            self.advance();
17801            if columns.is_some() {
17802                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
17803            }
17804            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17805            let returning = self.parse_optional_returning()?;
17806            return Ok(Statement::Insert(InsertStatement {
17807                ctes: Vec::new(),
17808                table,
17809                alias: alias.clone(),
17810                columns: Some(Vec::new()),
17811                rows: alloc::vec![Vec::new()],
17812                select_source: None,
17813                on_conflict,
17814                returning,
17815                overriding,
17816                mysql_ignore: ignore,
17817            }));
17818        }
17819        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
17820        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
17821        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
17822        // SELECT …`) heads the SOURCE select, as in PG (the statement's
17823        // own WITH comes before INSERT).
17824        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17825            let select_stmt = if self.peek_is_with_kw() {
17826                self.advance();
17827                self.parse_nested_with_select()?
17828            } else {
17829                match self.parse_select_stmt()? {
17830                    Statement::Select(s) => s,
17831                    other => {
17832                        return Err(self.err(alloc::format!(
17833                            "expected SELECT after INSERT INTO ... target, got {other:?}"
17834                        )));
17835                    }
17836                }
17837            };
17838            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17839            let returning = self.parse_optional_returning()?;
17840            return Ok(Statement::Insert(InsertStatement {
17841                ctes: Vec::new(),
17842                table,
17843                alias: alias.clone(),
17844                columns,
17845                rows: Vec::new(),
17846                select_source: Some(Box::new(select_stmt)),
17847                on_conflict,
17848                returning,
17849                overriding,
17850                mysql_ignore: ignore,
17851            }));
17852        }
17853        if !matches!(self.peek(), Token::Values) {
17854            return Err(self.err(format!(
17855                "expected VALUES or SELECT after table name, got {:?}",
17856                self.peek()
17857            )));
17858        }
17859        self.advance();
17860        if !matches!(self.peek(), Token::LParen) {
17861            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
17862        }
17863        let mut rows = Vec::new();
17864        loop {
17865            // Each iteration consumes one `(expr, expr, …)` tuple.
17866            if !matches!(self.peek(), Token::LParen) {
17867                return Err(self.err(format!(
17868                    "expected '(' for next VALUES tuple, got {:?}",
17869                    self.peek()
17870                )));
17871            }
17872            self.advance();
17873            let mut tuple = Vec::new();
17874            loop {
17875                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
17876                // the column's declared default for that slot. Rides out as the
17877                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
17878                // path uses; the INSERT executor resolves it per target column.
17879                if matches!(self.peek(), Token::Default) {
17880                    self.advance();
17881                    tuple.push(Expr::FunctionCall {
17882                        name: "__column_default".to_string(),
17883                        args: Vec::new(),
17884                    });
17885                } else {
17886                    tuple.push(self.parse_expr(0)?);
17887                }
17888                match self.peek() {
17889                    Token::Comma => {
17890                        self.advance();
17891                    }
17892                    Token::RParen => {
17893                        self.advance();
17894                        break;
17895                    }
17896                    other => {
17897                        return Err(self.err(format!(
17898                            "expected ',' or ')' in VALUES tuple, got {other:?}"
17899                        )));
17900                    }
17901                }
17902            }
17903            if tuple.is_empty() {
17904                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
17905            }
17906            rows.push(tuple);
17907            // Continue with comma-separated tuples.
17908            if matches!(self.peek(), Token::Comma) {
17909                self.advance();
17910            } else {
17911                break;
17912            }
17913        }
17914        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
17915        // to ON CONFLICT DO UPDATE with an empty conflict target
17916        // (the engine picks the table's first unique index, which
17917        // matches MySQL's any-unique-key behaviour for the common
17918        // single-key case). `VALUES(col)` in the assignments is
17919        // MySQL's spelling of EXCLUDED.col.
17920        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17921        let returning = self.parse_optional_returning()?;
17922        Ok(Statement::Insert(InsertStatement {
17923            ctes: Vec::new(),
17924            table,
17925            alias,
17926            columns,
17927            rows,
17928            select_source: None,
17929            on_conflict,
17930            returning,
17931            overriding,
17932            mysql_ignore: ignore,
17933        }))
17934    }
17935
17936    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
17937    /// the incoming row's value — exactly PG's EXCLUDED.col.
17938    fn rewrite_mysql_values_refs(e: &mut Expr) {
17939        match e {
17940            Expr::FunctionCall { name, args }
17941                if name.eq_ignore_ascii_case("values")
17942                    && args.len() == 1
17943                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
17944            {
17945                let Expr::Column(c) = &args[0] else {
17946                    unreachable!("guarded above");
17947                };
17948                *e = Expr::Column(crate::ast::ColumnName {
17949                    qualifier: Some("EXCLUDED".to_string()),
17950                    name: c.name.clone(),
17951                });
17952            }
17953            Expr::FunctionCall { args, .. } => {
17954                for a in args {
17955                    Self::rewrite_mysql_values_refs(a);
17956                }
17957            }
17958            Expr::Binary { lhs, rhs, .. } => {
17959                Self::rewrite_mysql_values_refs(lhs);
17960                Self::rewrite_mysql_values_refs(rhs);
17961            }
17962            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
17963                Self::rewrite_mysql_values_refs(expr);
17964            }
17965            Expr::Case {
17966                operand,
17967                branches,
17968                else_branch,
17969            } => {
17970                if let Some(op) = operand {
17971                    Self::rewrite_mysql_values_refs(op);
17972                }
17973                for (w, t) in branches {
17974                    Self::rewrite_mysql_values_refs(w);
17975                    Self::rewrite_mysql_values_refs(t);
17976                }
17977                if let Some(el) = else_branch {
17978                    Self::rewrite_mysql_values_refs(el);
17979                }
17980            }
17981            _ => {}
17982        }
17983    }
17984
17985    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
17986    /// clause sitting between the INSERT body and the trailing
17987    /// RETURNING. All keywords come in as bare idents; `ON` is
17988    /// a reserved Token though.
17989    fn parse_optional_on_conflict(
17990        &mut self,
17991    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17992        if !matches!(self.peek(), Token::On) {
17993            return Ok(None);
17994        }
17995        // Peek further: we want exactly "ON CONFLICT ...". If the
17996        // next ident isn't "conflict", let some other parser handle.
17997        let next_is_conflict = matches!(
17998            self.tokens.get(self.pos + 1),
17999            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18000        );
18001        if !next_is_conflict {
18002            return Ok(None);
18003        }
18004        self.advance(); // ON
18005        self.advance(); // CONFLICT
18006        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18007        // the constraint instead of listing columns (the pg_dump
18008        // form); the engine resolves it.
18009        let mut constraint_name: Option<String> = None;
18010        if matches!(self.peek(), Token::On) {
18011            self.advance(); // ON
18012            match self.advance() {
18013                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18014                }
18015                other => {
18016                    return Err(self.err(alloc::format!(
18017                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18018                    )));
18019                }
18020            }
18021            constraint_name = Some(self.expect_ident_like()?);
18022        }
18023        // Optional `(col [, col]*)` target list.
18024        let mut target_columns: Vec<String> = Vec::new();
18025        if matches!(self.peek(), Token::LParen) {
18026            self.advance();
18027            loop {
18028                target_columns.push(self.expect_ident_like()?);
18029                match self.peek() {
18030                    Token::Comma => {
18031                        self.advance();
18032                    }
18033                    Token::RParen => {
18034                        self.advance();
18035                        break;
18036                    }
18037                    other => {
18038                        return Err(self.err(alloc::format!(
18039                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18040                        )));
18041                    }
18042                }
18043            }
18044        }
18045        // v7.39 (round 240) — optional index predicate after the target
18046        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18047        // PARTIAL unique index; SPG's arbiters are full indexes, which
18048        // satisfy any predicate, so it is parsed and carried but not
18049        // consulted (recorded residual: partial-unique-index arbiters).
18050        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18051            self.advance();
18052            Some(self.parse_expr(0)?)
18053        } else {
18054            None
18055        };
18056        // Required `DO`.
18057        match self.advance() {
18058            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18059            other => {
18060                return Err(self.err(alloc::format!(
18061                    "expected DO after ON CONFLICT [(…)], got {other:?}"
18062                )));
18063            }
18064        }
18065        // Action: NOTHING | UPDATE SET …
18066        let action = match self.advance() {
18067            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18068                crate::ast::OnConflictAction::Nothing
18069            }
18070            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18071                self.parse_on_conflict_update_action()?
18072            }
18073            other => {
18074                return Err(self.err(alloc::format!(
18075                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18076                )));
18077            }
18078        };
18079        Ok(Some(crate::ast::OnConflictClause {
18080            target_columns,
18081            index_where,
18082            constraint_name,
18083            mysql_lowered: false,
18084            action,
18085        }))
18086    }
18087
18088    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18089    /// `SET col = expr [, …] [WHERE cond]`. Caller already
18090    /// consumed `UPDATE`.
18091    fn parse_on_conflict_update_action(
18092        &mut self,
18093    ) -> Result<crate::ast::OnConflictAction, ParseError> {
18094        // `SET`
18095        match self.advance() {
18096            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18097            other => {
18098                return Err(self.err(alloc::format!(
18099                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18100                )));
18101            }
18102        }
18103        let mut assignments: Vec<(String, Expr)> = Vec::new();
18104        loop {
18105            let col = self.expect_ident_like()?;
18106            if !matches!(self.peek(), Token::Eq) {
18107                return Err(self.err(alloc::format!(
18108                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18109                    self.peek()
18110                )));
18111            }
18112            self.advance();
18113            let value = self.parse_expr(0)?;
18114            assignments.push((col, value));
18115            if matches!(self.peek(), Token::Comma) {
18116                self.advance();
18117                continue;
18118            }
18119            break;
18120        }
18121        let where_ = if matches!(self.peek(), Token::Where) {
18122            self.advance();
18123            Some(self.parse_expr(0)?)
18124        } else {
18125            None
18126        };
18127        Ok(crate::ast::OnConflictAction::Update {
18128            assignments,
18129            where_,
18130        })
18131    }
18132
18133    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18134        let mut items = Vec::new();
18135        // v7.39 (round 341, V66) — PG's target list may be EMPTY
18136        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18137        // answers one zero-column row per row of t, and a bare `SELECT`
18138        // answers a single zero-column row. SPG required at least one
18139        // item, so both were syntax errors. Recognised by the token that
18140        // follows — nothing that can start an expression appears here.
18141        if self.select_list_is_empty_here() {
18142            return Ok(items);
18143        }
18144        loop {
18145            items.push(self.parse_select_item()?);
18146            if matches!(self.peek(), Token::Comma) {
18147                self.advance();
18148            } else {
18149                break;
18150            }
18151        }
18152        Ok(items)
18153    }
18154
18155    /// Is the target list empty at this point — i.e. does the next token
18156    /// end the SELECT's item list rather than start an item?
18157    fn select_list_is_empty_here(&self) -> bool {
18158        match self.peek() {
18159            Token::From
18160            | Token::Where
18161            | Token::Group
18162            | Token::Having
18163            | Token::Order
18164            | Token::Limit
18165            | Token::Offset
18166            | Token::Semicolon
18167            | Token::RParen
18168            | Token::Union
18169            | Token::Except
18170            | Token::Eof => true,
18171            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18172            // with unreserved keywords, so they arrive as plain idents.
18173            Token::Ident(s) => {
18174                s.eq_ignore_ascii_case("fetch")
18175                    || s.eq_ignore_ascii_case("window")
18176                    || s.eq_ignore_ascii_case("intersect")
18177            }
18178            _ => false,
18179        }
18180    }
18181
18182    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18183        if matches!(self.peek(), Token::Star) {
18184            self.advance();
18185            return Ok(SelectItem::Wildcard);
18186        }
18187        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18188        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18189        // choke on the `*` ("expected identifier, got Star"). The lookahead is
18190        // `<ident> . *` with nothing binding tighter.
18191        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18192            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18193                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18194            {
18195                self.advance(); // qualifier
18196                self.advance(); // .
18197                self.advance(); // *
18198                return Ok(SelectItem::QualifiedWildcard(q));
18199            }
18200        }
18201        let start_tok = self.pos;
18202        let expr = self.parse_expr(0)?;
18203        let end_tok = self.consumed_pos();
18204        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18205        // multi-column function returns into columns. Marked here and lowered in
18206        // `parse_bare_select`, where the FROM clause is in hand.
18207        if matches!(self.peek(), Token::Dot)
18208            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18209        {
18210            self.advance(); // .
18211            self.advance(); // *
18212            return Ok(SelectItem::Expr {
18213                expr: Expr::FunctionCall {
18214                    name: "__record_expand".to_string(),
18215                    args: alloc::vec![expr],
18216                },
18217                alias: None,
18218            });
18219        }
18220        let alias = match self.parse_optional_alias()? {
18221            Some(a) => Some(a),
18222            None => self.mysql_item_label(&expr, start_tok, end_tok),
18223        };
18224        Ok(SelectItem::Expr { expr, alias })
18225    }
18226
18227    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18228    /// carries no `AS`, filled in here so every downstream path reports it
18229    /// without knowing the rule. `None` leaves the item un-aliased, which is
18230    /// what a PG session always gets.
18231    ///
18232    /// Measured against MariaDB 11, three rules and no more:
18233    ///
18234    /// | item             | label      | why                          |
18235    /// |------------------|------------|------------------------------|
18236    /// | `lbl.a`          | `a`        | a column reports its name    |
18237    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
18238    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
18239    ///
18240    /// The third is why this lives in the parser at all: the label is the
18241    /// text the client WROTE, down to the spacing, so it cannot be printed
18242    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18243    ///
18244    /// Comments survive, and that is right: through a `mariadb` CLI both
18245    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
18246    /// CLIENT stripping the comment before it sends. Asked over the raw
18247    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18248    /// produces.
18249    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18250        if !self.mysql_dialect {
18251            return None;
18252        }
18253        match expr {
18254            // A column already reports its own name downstream; naming it
18255            // again here would only re-state the qualifier the label drops.
18256            Expr::Column(_) => None,
18257            Expr::Literal(Literal::String(v)) => Some(v.clone()),
18258            _ => self.source_span(start_tok, end_tok).map(str::to_string),
18259        }
18260    }
18261
18262    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18263    /// consumed VALUES keyword. Each row lowers to a constant SELECT
18264    /// with PG's default column1..columnN names; subsequent rows
18265    /// chain as UNION ALL peers. Shared by the FROM-position
18266    /// `( VALUES … )` arm and the top-level bare VALUES statement.
18267    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18268        let mut row_selects: Vec<SelectStatement> = Vec::new();
18269        loop {
18270            if !matches!(self.peek(), Token::LParen) {
18271                return Err(self.err(alloc::format!(
18272                    "expected '(' to start a VALUES row, got {:?}",
18273                    self.peek()
18274                )));
18275            }
18276            self.advance(); // (
18277            let mut items: Vec<SelectItem> = Vec::new();
18278            loop {
18279                let expr = self.parse_expr(0)?;
18280                items.push(SelectItem::Expr {
18281                    expr,
18282                    alias: Some(alloc::format!("column{}", items.len() + 1)),
18283                });
18284                match self.peek() {
18285                    Token::Comma => {
18286                        self.advance();
18287                    }
18288                    Token::RParen => break,
18289                    other => {
18290                        return Err(self.err(alloc::format!(
18291                            "expected ',' or ')' in VALUES row, got {other:?}"
18292                        )));
18293                    }
18294                }
18295            }
18296            self.advance(); // )
18297            row_selects.push(SelectStatement {
18298                locking: None,
18299                ctes: Vec::new(),
18300                distinct: false,
18301                distinct_on: Vec::new(),
18302                items,
18303                from: None,
18304                where_: None,
18305                group_by: None,
18306                group_by_all: false,
18307                having: None,
18308                unions: Vec::new(),
18309                order_by: Vec::new(),
18310                limit: None,
18311                offset: None,
18312                limit_with_ties: false,
18313                window_check_exprs: Vec::new(),
18314            });
18315            if matches!(self.peek(), Token::Comma) {
18316                self.advance();
18317                continue;
18318            }
18319            break;
18320        }
18321        let mut head = row_selects.remove(0);
18322        head.unions = row_selects
18323            .into_iter()
18324            .map(|s| (UnionKind::All, s))
18325            .collect();
18326        Ok(head)
18327    }
18328
18329    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18330        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18331        // children. It was read as a table NAMED `only`, so the query
18332        // failed on `relation "only" does not exist`.
18333        //
18334        // v7.39 (round 644) — and it is no longer a no-op. Round 621
18335        // absorbed the keyword, reasoning that SPG's children are
18336        // separate relations a plain scan does not descend into, so ONLY
18337        // already described the scan. That stopped being true when a
18338        // partition parent started unioning its children: measured,
18339        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18340        // where PG answers 0. The flag is carried now.
18341        let mut only = false;
18342        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18343            && matches!(
18344                self.tokens.get(self.pos + 1),
18345                Some(Token::Ident(_) | Token::QuotedIdent(_))
18346            )
18347        {
18348            only = true;
18349            self.advance();
18350        }
18351        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18352        // for these SRFs the keyword is noise at parse time: the
18353        // join executor already substitutes outer-column references
18354        // into unnest_expr / generate_series_args per outer row
18355        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18356        // licences the correlation even without the keyword. Absorb
18357        // it and fall through to the SRF arms below.
18358        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18359        // just the four builtin SRFs: a user set-returning function on a JOIN's
18360        // right side is the whole point of LATERAL. The keyword stays noise at
18361        // parse time — the join executor substitutes the outer row into the
18362        // call's arguments per outer row.
18363        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18364            && matches!(
18365                self.tokens.get(self.pos + 1),
18366                // The json_each family has its OWN `LATERAL …` arm below, which
18367                // needs to see the keyword — absorbing it here would send those
18368                // calls down the generic table-function channel instead.
18369                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18370            )
18371            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18372        {
18373            self.advance(); // LATERAL
18374        }
18375        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18376        // set-returning function whose argument may reference a
18377        // preceding FROM item. We rewrite this to
18378        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18379        // AS __srf__) AS <alias>` so the existing LATERAL subquery
18380        // executor handles per-outer-row evaluation and the
18381        // SRF-primary jsonb_each_text path handles the inner
18382        // materialisation. Sentori 0067 backfill is the dogfood
18383        // shape.
18384        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18385            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18386            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18387        {
18388            self.advance(); // LATERAL
18389            let each_fn = match self.peek() {
18390                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18391                _ => unreachable!(),
18392            };
18393            self.advance(); // jsonb_each[_text] / json_each[_text]
18394            self.advance(); // (
18395            let arg = self.parse_expr(0)?;
18396            if !matches!(self.peek(), Token::RParen) {
18397                return Err(self.err(alloc::format!(
18398                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18399                    self.peek()
18400                )));
18401            }
18402            self.advance();
18403            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18404            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18405            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18406            //               FROM jsonb_each_text(<arg>) AS __srf__
18407            // PG's `AS kv(key, value)` column-alias list maps
18408            // positions to names; default to (key, value) when
18409            // omitted (matching the SRF's natural column names).
18410            let srf_alias = "__srf__".to_string();
18411            let key_alias = column_aliases
18412                .first()
18413                .cloned()
18414                .unwrap_or_else(|| "key".to_string());
18415            let value_alias = column_aliases
18416                .get(1)
18417                .cloned()
18418                .unwrap_or_else(|| "value".to_string());
18419            let inner_select = crate::ast::SelectStatement {
18420                locking: None,
18421                ctes: Vec::new(),
18422                distinct: false,
18423                distinct_on: Vec::new(),
18424                items: alloc::vec![
18425                    crate::ast::SelectItem::Expr {
18426                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18427                            qualifier: Some(srf_alias.clone()),
18428                            name: "key".to_string(),
18429                        }),
18430                        alias: Some(key_alias),
18431                    },
18432                    crate::ast::SelectItem::Expr {
18433                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18434                            qualifier: Some(srf_alias.clone()),
18435                            name: "value".to_string(),
18436                        }),
18437                        alias: Some(value_alias),
18438                    },
18439                ],
18440                from: Some(crate::ast::FromClause {
18441                    primary: TableRef {
18442                        name: srf_alias.clone(),
18443                        alias: Some(srf_alias.clone()),
18444                        only: false,
18445                        as_of_segment: None,
18446                        unnest_expr: None,
18447                        unnest_column_aliases: Vec::new(),
18448                        with_ordinality: false,
18449                        generate_series_args: None,
18450                        lateral_subquery: None,
18451                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18452                        table_fn_call: None,
18453                        rows_from: None,
18454                        json_table: None,
18455                        scalar_fn_item: false,
18456                    },
18457                    joins: Vec::new(),
18458                }),
18459                where_: None,
18460                group_by: None,
18461                group_by_all: false,
18462                having: None,
18463                unions: Vec::new(),
18464                order_by: Vec::new(),
18465                limit: None,
18466                offset: None,
18467                limit_with_ties: false,
18468                window_check_exprs: Vec::new(),
18469            };
18470            return Ok(TableRef {
18471                name: alias.clone(),
18472                alias: Some(alias),
18473                only: false,
18474                as_of_segment: None,
18475                unnest_expr: None,
18476                unnest_column_aliases: Vec::new(),
18477                with_ordinality: false,
18478                generate_series_args: None,
18479                lateral_subquery: Some(Box::new(inner_select)),
18480                jsonb_each_text_arg: None,
18481                table_fn_call: None,
18482                rows_from: None,
18483                json_table: None,
18484                scalar_fn_item: false,
18485            });
18486        }
18487        // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18488        // without an explicit `LATERAL` keyword is the same shape
18489        // PG accepts (SRF naturally licences lateral correlation).
18490        // We mirror the LATERAL rewrite when the argument syntactic-
18491        // ally references an outer column (Column { qualifier:
18492        // Some(_), … }). For simplicity we apply the rewrite
18493        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18494        // in the FROM-list — caller-side join parsing positions
18495        // this peek correctly.
18496        // (Implementation note: detection lives below; the LATERAL
18497        // branch above already covers the explicit form; the bare
18498        // form falls through to the plain SRF arm and the engine
18499        // treats it as a constant-arg SRF if no outer reference is
18500        // present.)
18501        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18502        // table. Detect at the head so it claims precedence over
18503        // every other table-ref shape (unnest / generate_series /
18504        // bare ident); the lateral subquery itself follows the
18505        // regular SELECT grammar.
18506        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18507        // t(cols)`. Each row lowers to a constant SELECT with PG's
18508        // default column1..columnN names; subsequent rows chain as
18509        // UNION ALL peers. The result rides the derived-table
18510        // lateral_subquery channel — zero executor work.
18511        if matches!(self.peek(), Token::LParen)
18512            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18513        {
18514            self.advance(); // (
18515            self.advance(); // VALUES
18516            let head = self.parse_values_rows_body()?;
18517            if !matches!(self.peek(), Token::RParen) {
18518                return Err(self.err(alloc::format!(
18519                    "expected ')' after VALUES list, got {:?}",
18520                    self.peek()
18521                )));
18522            }
18523            self.advance();
18524            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18525            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18526            return Ok(TableRef {
18527                name,
18528                alias: alias_ident,
18529                only: false,
18530                as_of_segment: None,
18531                unnest_expr: None,
18532                unnest_column_aliases: column_aliases,
18533                with_ordinality: false,
18534                generate_series_args: None,
18535                lateral_subquery: Some(Box::new(head)),
18536                jsonb_each_text_arg: None,
18537                table_fn_call: None,
18538                rows_from: None,
18539                json_table: None,
18540                scalar_fn_item: false,
18541            });
18542        }
18543        // v7.37.17 (17.6 siblings) — plain derived table:
18544        // `FROM ( SELECT … ) [AS] alias`. Rides the same
18545        // lateral_subquery channel the explicit LATERAL form uses —
18546        // an uncorrelated inner SELECT executes identically. The
18547        // inner parse carries UNION tails (they live on
18548        // SelectStatement.unions).
18549        // v7.37 D.20 — the derived-table inner may itself be a
18550        // parenthesized set-operation group (`FROM ((SELECT…) UNION
18551        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18552        // bare `(SELECT …)`. parse_one_statement already routes a leading
18553        // `(` set-op group (its LParen arm) and a leading WITH
18554        // (parse_with_cte_then_select), so widen the second-token gate to
18555        // Select | LParen | WITH.
18556        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18557        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18558        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18559        // has existed since the shorthand landed and `parse_bare_select`
18560        // already routes it ("valid anywhere a SELECT head is"); what was
18561        // missing is this second-token gate, and the CTE body's dispatch
18562        // below. Round 868 found both by putting the shorthand in a
18563        // subquery — the top-level forms had been the only ones tested.
18564        if matches!(self.peek(), Token::LParen)
18565            && (matches!(
18566                self.tokens.get(self.pos + 1),
18567                Some(Token::Select | Token::LParen | Token::Table)
18568            ) || matches!(self.tokens.get(self.pos + 1),
18569                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18570        {
18571            self.advance(); // (
18572            let inner = match self.parse_one_statement()? {
18573                Statement::Select(s) => s,
18574                other => {
18575                    return Err(self.err(alloc::format!(
18576                        "expected SELECT inside derived table ( … ), got {other:?}"
18577                    )));
18578                }
18579            };
18580            if !matches!(self.peek(), Token::RParen) {
18581                return Err(self.err(alloc::format!(
18582                    "expected ')' after derived-table subquery, got {:?}",
18583                    self.peek()
18584                )));
18585            }
18586            self.advance();
18587            // `AS t(a, b)` column-alias list rides the
18588            // unnest_column_aliases field (same positional-rename
18589            // contract the unnest SRFs use).
18590            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18591            let name = alias_ident
18592                .clone()
18593                .unwrap_or_else(|| "subquery".to_string());
18594            return Ok(TableRef {
18595                name,
18596                alias: alias_ident,
18597                only: false,
18598                as_of_segment: None,
18599                unnest_expr: None,
18600                unnest_column_aliases: column_aliases,
18601                with_ordinality: false,
18602                generate_series_args: None,
18603                lateral_subquery: Some(Box::new(inner)),
18604                jsonb_each_text_arg: None,
18605                table_fn_call: None,
18606                rows_from: None,
18607                json_table: None,
18608                scalar_fn_item: false,
18609            });
18610        }
18611        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18612            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18613        {
18614            self.advance(); // LATERAL
18615            self.advance(); // (
18616            // Parse the inner SELECT.
18617            let inner = match self.parse_one_statement()? {
18618                Statement::Select(s) => s,
18619                other => {
18620                    return Err(self.err(alloc::format!(
18621                        "expected SELECT inside LATERAL ( … ), got {other:?}"
18622                    )));
18623                }
18624            };
18625            if !matches!(self.peek(), Token::RParen) {
18626                return Err(self.err(alloc::format!(
18627                    "expected ')' after LATERAL subquery, got {:?}",
18628                    self.peek()
18629                )));
18630            }
18631            self.advance();
18632            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
18633            // `(VALUES …) t(g)` derived table round-trips through view-body
18634            // Display, which renders on the lateral_subquery channel).
18635            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18636            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
18637            return Ok(TableRef {
18638                name,
18639                alias: alias_ident,
18640                only: false,
18641                as_of_segment: None,
18642                unnest_expr: None,
18643                unnest_column_aliases: column_aliases,
18644                with_ordinality: false,
18645                generate_series_args: None,
18646                lateral_subquery: Some(Box::new(inner)),
18647                jsonb_each_text_arg: None,
18648                table_fn_call: None,
18649                rows_from: None,
18650                json_table: None,
18651                scalar_fn_item: false,
18652            });
18653        }
18654        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
18655        // function as a FROM item. Emits one row per (key, value)
18656        // pair in the JSONB object argument as TEXT columns. May
18657        // be wrapped in CROSS JOIN LATERAL when the argument
18658        // references a preceding FROM item (sentori migration
18659        // 0067 backfill shape: `CROSS JOIN LATERAL
18660        // jsonb_each_text(t.json_col) AS kv(key, value)`).
18661        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
18662            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18663        {
18664            let each_fn = match self.peek() {
18665                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18666                _ => unreachable!(),
18667            };
18668            self.advance(); // jsonb_each[_text] / json_each[_text]
18669            self.advance(); // (
18670            let arg = self.parse_expr(0)?;
18671            if !matches!(self.peek(), Token::RParen) {
18672                return Err(self.err(alloc::format!(
18673                    "expected ')' after {each_fn}() argument, got {:?}",
18674                    self.peek()
18675                )));
18676            }
18677            self.advance();
18678            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18679            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18680            return Ok(TableRef {
18681                name,
18682                alias: alias_ident,
18683                only: false,
18684                as_of_segment: None,
18685                unnest_expr: None,
18686                // `AS t(k, v)` renames key/value positionally, same as the
18687                // LATERAL-position form already does.
18688                unnest_column_aliases: column_aliases,
18689                with_ordinality: false,
18690                generate_series_args: None,
18691                lateral_subquery: None,
18692                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18693                table_fn_call: None,
18694                rows_from: None,
18695                json_table: None,
18696                scalar_fn_item: false,
18697            });
18698        }
18699        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
18700        // (+ json_ variants) — record-returning JSON functions with a
18701        // column-definition list. Desugar to a derived table that
18702        // projects each declared column from the JSON via `->>` + a cast,
18703        // over `jsonb_array_elements(J)` for the *set (per-element) form.
18704        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
18705            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18706        {
18707            return self.parse_json_to_record_from();
18708        }
18709        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
18710        // row is a text[] of capture groups, so it cannot desugar to unnest
18711        // (that would flatten the array). Wrap it as a derived table
18712        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
18713        // SRF path already emits one text[] row per match. PG names the column
18714        // `regexp_matches`; an `AS a(col)` alias overrides it.
18715        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18716                if s.eq_ignore_ascii_case("regexp_matches"))
18717            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18718        {
18719            self.advance(); // fn name
18720            self.advance(); // (
18721            let mut fn_args: Vec<Expr> = Vec::new();
18722            loop {
18723                fn_args.push(self.parse_expr(0)?);
18724                if matches!(self.peek(), Token::Comma) {
18725                    self.advance();
18726                    continue;
18727                }
18728                break;
18729            }
18730            if !matches!(self.peek(), Token::RParen) {
18731                return Err(self.err(alloc::format!(
18732                    "expected ')' after regexp_matches() arguments, got {:?}",
18733                    self.peek()
18734                )));
18735            }
18736            self.advance();
18737            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
18738            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
18739            // it, so it died on the `with` token while every other table function
18740            // accepted it.
18741            let with_ordinality = self.absorb_with_ordinality();
18742            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18743            let table_alias = alias_ident
18744                .clone()
18745                .unwrap_or_else(|| "regexp_matches".to_string());
18746            // PG names a single-column function's output column after the ALIAS
18747            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
18748            // `m` reads as that column and not as a whole-row composite. Naming
18749            // it after the function regardless made `SELECT m[1] FROM … AS m`
18750            // subscript a record.
18751            let col_name = column_aliases
18752                .first()
18753                .cloned()
18754                .or_else(|| alias_ident.clone())
18755                .unwrap_or_else(|| "regexp_matches".to_string());
18756            let inner = crate::ast::SelectStatement {
18757                locking: None,
18758                ctes: Vec::new(),
18759                distinct: false,
18760                distinct_on: Vec::new(),
18761                items: alloc::vec![SelectItem::Expr {
18762                    expr: Expr::FunctionCall {
18763                        name: "regexp_matches".to_string(),
18764                        args: fn_args,
18765                    },
18766                    alias: Some(col_name),
18767                }],
18768                from: None,
18769                where_: None,
18770                group_by: None,
18771                group_by_all: false,
18772                having: None,
18773                unions: Vec::new(),
18774                order_by: Vec::new(),
18775                limit: None,
18776                offset: None,
18777                limit_with_ties: false,
18778                window_check_exprs: Vec::new(),
18779            };
18780            return Ok(TableRef {
18781                name: table_alias.clone(),
18782                alias: Some(table_alias),
18783                only: false,
18784                as_of_segment: None,
18785                unnest_expr: None,
18786                unnest_column_aliases: column_aliases,
18787                with_ordinality,
18788                generate_series_args: None,
18789                lateral_subquery: Some(Box::new(inner)),
18790                jsonb_each_text_arg: None,
18791                table_fn_call: None,
18792                rows_from: None,
18793                json_table: None,
18794                // regexp_matches returns text[], a base type: `SELECT m FROM
18795                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
18796                scalar_fn_item: true,
18797            });
18798        }
18799        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
18800        // / json_ variants as a FROM item. Rewritten into
18801        // `unnest(<same fn>(<expr>))`: the scalar form returns the
18802        // elements as a TEXT array, and the existing unnest SRF path
18803        // materialises one row per element. PG's natural column name
18804        // is `value`; an `AS a(col)` column-alias list overrides it.
18805        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18806                if s.eq_ignore_ascii_case("jsonb_array_elements")
18807                    || s.eq_ignore_ascii_case("json_array_elements")
18808                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
18809                    || s.eq_ignore_ascii_case("json_array_elements_text")
18810                    || s.eq_ignore_ascii_case("jsonb_object_keys")
18811                    || s.eq_ignore_ascii_case("json_object_keys")
18812                    || s.eq_ignore_ascii_case("jsonb_path_query")
18813                    || s.eq_ignore_ascii_case("json_path_query")
18814                    || s.eq_ignore_ascii_case("generate_subscripts")
18815                    || s.eq_ignore_ascii_case("string_to_table")
18816                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
18817            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18818        {
18819            let fn_name = match self.peek() {
18820                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18821                _ => unreachable!(),
18822            };
18823            self.advance(); // fn name
18824            self.advance(); // (
18825            let mut fn_args: Vec<Expr> = Vec::new();
18826            loop {
18827                fn_args.push(self.parse_expr(0)?);
18828                if matches!(self.peek(), Token::Comma) {
18829                    self.advance();
18830                    continue;
18831                }
18832                break;
18833            }
18834            if !matches!(self.peek(), Token::RParen) {
18835                return Err(self.err(alloc::format!(
18836                    "expected ')' after {fn_name}() arguments, got {:?}",
18837                    self.peek()
18838                )));
18839            }
18840            self.advance();
18841            let with_ordinality = self.absorb_with_ordinality();
18842            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18843            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
18844            // PG's natural column name: the array-elements SRFs
18845            // declare an OUT parameter `value`; jsonb_object_keys
18846            // and generate_subscripts have none, so the column is
18847            // named after the function. A bare table alias on a
18848            // single-column SRF renames the column too (PG: `FROM
18849            // generate_subscripts(a, 1) AS s` projects column s) —
18850            // except for the OUT-parameter SRFs, whose column stays
18851            // `value` under a bare alias.
18852            let natural_col = if fn_name.ends_with("_array_elements")
18853                || fn_name.ends_with("_array_elements_text")
18854            {
18855                "value".to_string()
18856            } else {
18857                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
18858            };
18859            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
18860            // Keep any further entries — the second names the
18861            // ordinality column under WITH ORDINALITY.
18862            srf_cols.extend(column_aliases.into_iter().skip(1));
18863            // The *_to_table SRFs are row-streams over the existing
18864            // *_to_array scalars — map the call target; the display
18865            // name (alias / column defaults) keeps the SRF spelling.
18866            let call_name = match fn_name.as_str() {
18867                "string_to_table" => "string_to_array".to_string(),
18868                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
18869                _ => fn_name,
18870            };
18871            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
18872            // preceding FROM item (bare or qualified column) is correlated;
18873            // route it through the per-outer-row lateral channel.
18874            let expr = crate::ast::Expr::FunctionCall {
18875                name: call_name,
18876                args: fn_args,
18877            };
18878            let correlated = Self::expr_has_any_column(&expr);
18879            let tref = TableRef {
18880                name,
18881                alias: alias_ident,
18882                only: false,
18883                as_of_segment: None,
18884                unnest_expr: Some(Box::new(expr)),
18885                unnest_column_aliases: srf_cols,
18886                with_ordinality,
18887                generate_series_args: None,
18888                lateral_subquery: None,
18889                jsonb_each_text_arg: None,
18890                table_fn_call: None,
18891                rows_from: None,
18892                json_table: None,
18893                // Each of these returns a BASE type (jsonb / text / int), so the item's
18894                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
18895                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
18896                scalar_fn_item: !with_ordinality,
18897            };
18898            return Ok(if correlated {
18899                Self::wrap_correlated_srf(tref)
18900            } else {
18901                tref
18902            });
18903        }
18904        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
18905        // explicit parallel-zip syntax. Each entry lowers to its
18906        // array-returning scalar form (unnest(x) → x itself; the
18907        // FROM-SRF rewrite family → their scalar array calls) and
18908        // the list rides the multi-arg unnest zip channel:
18909        // NULL-padded to the longest, WITH ORDINALITY appends the
18910        // counter. generate_series has no scalar array form and
18911        // errors honestly.
18912        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
18913            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
18914            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18915        {
18916            self.advance(); // ROWS
18917            self.advance(); // FROM
18918            self.advance(); // (
18919            let mut entries: Vec<Expr> = Vec::new();
18920            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
18921            // Used only when some entry has no array form.
18922            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
18923            loop {
18924                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
18925                if !matches!(self.peek(), Token::LParen) {
18926                    return Err(self.err(alloc::format!(
18927                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
18928                        self.peek()
18929                    )));
18930                }
18931                self.advance();
18932                let mut fn_args: Vec<Expr> = Vec::new();
18933                if !matches!(self.peek(), Token::RParen) {
18934                    loop {
18935                        fn_args.push(self.parse_expr(0)?);
18936                        if matches!(self.peek(), Token::Comma) {
18937                            self.advance();
18938                            continue;
18939                        }
18940                        break;
18941                    }
18942                }
18943                if !matches!(self.peek(), Token::RParen) {
18944                    return Err(self.err(alloc::format!(
18945                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
18946                        self.peek()
18947                    )));
18948                }
18949                self.advance();
18950                let entry = match fn_name.as_str() {
18951                    "unnest" => {
18952                        if fn_args.len() != 1 {
18953                            return Err(
18954                                self.err("unnest inside ROWS FROM takes exactly one array".into())
18955                            );
18956                        }
18957                        fn_args.pop().expect("len checked")
18958                    }
18959                    "jsonb_array_elements"
18960                    | "json_array_elements"
18961                    | "jsonb_array_elements_text"
18962                    | "json_array_elements_text"
18963                    | "jsonb_object_keys"
18964                    | "json_object_keys"
18965                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
18966                        name: fn_name,
18967                        args: fn_args,
18968                    },
18969                    "string_to_table" => crate::ast::Expr::FunctionCall {
18970                        name: "string_to_array".to_string(),
18971                        args: fn_args,
18972                    },
18973                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
18974                        name: "regexp_split_to_array".to_string(),
18975                        args: fn_args,
18976                    },
18977                    // v7.39 (read01 round 74) — an SRF with no array form
18978                    // (`generate_series`, a user `RETURNS SETOF` function) has no
18979                    // scalar expression to zip, so the WHOLE list switches to the
18980                    // rows_from channel, which runs each function and zips the
18981                    // rows themselves. The all-array case keeps the old lowering:
18982                    // it is well-trodden and this must not disturb it.
18983                    _ => {
18984                        generic.push((fn_name, fn_args));
18985                        if matches!(self.peek(), Token::Comma) {
18986                            self.advance();
18987                            continue;
18988                        }
18989                        break;
18990                    }
18991                };
18992                generic.push((
18993                    // The array-able entries carry their lowered expr along, so a
18994                    // MIXED list still works: the engine sees the scalar array
18995                    // form and unnests it.
18996                    "__array".to_string(),
18997                    alloc::vec![entry.clone()],
18998                ));
18999                entries.push(entry);
19000                if matches!(self.peek(), Token::Comma) {
19001                    self.advance();
19002                    continue;
19003                }
19004                break;
19005            }
19006            if !matches!(self.peek(), Token::RParen) {
19007                return Err(self.err(alloc::format!(
19008                    "expected ')' to close ROWS FROM, got {:?}",
19009                    self.peek()
19010                )));
19011            }
19012            self.advance();
19013            let with_ordinality = self.absorb_with_ordinality();
19014            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19015            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19016            // v7.39 (read01 round 74) — some entry had no array form, so the whole
19017            // list rides the generic channel.
19018            if generic.iter().any(|(n, _)| n != "__array") {
19019                let correlated = generic
19020                    .iter()
19021                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19022                let tref = TableRef {
19023                    name,
19024                    alias: alias_ident,
19025                    only: false,
19026                    as_of_segment: None,
19027                    unnest_expr: None,
19028                    unnest_column_aliases,
19029                    with_ordinality,
19030                    generate_series_args: None,
19031                    lateral_subquery: None,
19032                    jsonb_each_text_arg: None,
19033                    table_fn_call: None,
19034                    rows_from: Some(generic),
19035                    json_table: None,
19036                    scalar_fn_item: false,
19037                };
19038                return Ok(if correlated {
19039                    Self::wrap_correlated_srf(tref)
19040                } else {
19041                    tref
19042                });
19043            }
19044            let correlated = entries.iter().any(Self::expr_has_any_column);
19045            let expr = if entries.len() == 1 {
19046                entries.pop().expect("len checked")
19047            } else {
19048                crate::ast::Expr::FunctionCall {
19049                    name: "__unnest_zip".to_string(),
19050                    args: entries,
19051                }
19052            };
19053            let tref = TableRef {
19054                name,
19055                alias: alias_ident,
19056                only: false,
19057                as_of_segment: None,
19058                unnest_expr: Some(Box::new(expr)),
19059                unnest_column_aliases,
19060                with_ordinality,
19061                generate_series_args: None,
19062                lateral_subquery: None,
19063                jsonb_each_text_arg: None,
19064                table_fn_call: None,
19065                rows_from: None,
19066                json_table: None,
19067                scalar_fn_item: false,
19068            };
19069            return Ok(if correlated {
19070                Self::wrap_correlated_srf(tref)
19071            } else {
19072                tref
19073            });
19074        }
19075        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19076        // source. Detect at the head before the bare-ident fallback;
19077        // unnest is not a reserved token.
19078        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19079            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19080        {
19081            self.advance(); // unnest
19082            self.advance(); // (
19083            let mut srf_args = alloc::vec![self.parse_expr(0)?];
19084            while matches!(self.peek(), Token::Comma) {
19085                self.advance();
19086                srf_args.push(self.parse_expr(0)?);
19087            }
19088            if !matches!(self.peek(), Token::RParen) {
19089                return Err(self.err(alloc::format!(
19090                    "expected ')' after unnest() argument, got {:?}",
19091                    self.peek()
19092                )));
19093            }
19094            self.advance();
19095            // Multi-arg unnest(a, b, …) zips the arrays in
19096            // parallel, NULL-padding to the longest (PG's ROWS
19097            // FROM shorthand). Lower onto the unnest channel as an
19098            // internal marker call the executors unpack.
19099            let expr = if srf_args.len() == 1 {
19100                srf_args.pop().expect("len checked")
19101            } else {
19102                crate::ast::Expr::FunctionCall {
19103                    name: "__unnest_zip".to_string(),
19104                    args: srf_args,
19105                }
19106            };
19107            let with_ordinality = self.absorb_with_ordinality();
19108            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19109            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19110            let correlated = Self::expr_has_any_column(&expr);
19111            let tref = TableRef {
19112                name,
19113                alias: alias_ident,
19114                only: false,
19115                as_of_segment: None,
19116                unnest_expr: Some(Box::new(expr)),
19117                unnest_column_aliases,
19118                with_ordinality,
19119                generate_series_args: None,
19120                lateral_subquery: None,
19121                jsonb_each_text_arg: None,
19122                table_fn_call: None,
19123                rows_from: None,
19124                json_table: None,
19125                scalar_fn_item: false,
19126            };
19127            return Ok(if correlated {
19128                Self::wrap_correlated_srf(tref)
19129            } else {
19130                tref
19131            });
19132        }
19133        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19134        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19135        // generic table-fn arg parser can't read), so it is intercepted
19136        // here BEFORE the generic dispatch. The doc expr may reference
19137        // outer columns (implicit LATERAL) — same correlated-wrap rule.
19138        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19139                if s.eq_ignore_ascii_case("json_table"))
19140            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19141        {
19142            let tref = self.parse_json_table_ref()?;
19143            let correlated = tref
19144                .json_table
19145                .as_deref()
19146                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19147            return Ok(if correlated {
19148                Self::wrap_correlated_srf(tref)
19149            } else {
19150                tref
19151            });
19152        }
19153        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19154        // functions dispatched by name (`pg_partition_tree('t')`,
19155        // `pg_partition_ancestors('t')`). Same head-detection shape as
19156        // unnest; the engine executor owns the row shape per function.
19157        // v7.39 (read01 round 65) — and a USER function in FROM position
19158        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19159        // (generate_series / unnest / the json_each family) keep it — their arms
19160        // sit further down, so they are excluded here by name rather than by
19161        // ordering. Anything else that is an ident followed by `(` is a table
19162        // function; the engine executor decides whether it is a builtin, a
19163        // set-returning user function, or an error.
19164        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19165                if !s.eq_ignore_ascii_case("generate_series")
19166                    && !s.eq_ignore_ascii_case("unnest")
19167                    && !is_json_each_name(s))
19168            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19169        {
19170            // Body out-of-line — this parse sits on the FROM/subquery
19171            // recursion chain (debug frame-cliff discipline).
19172            // v7.39 (read01 round 69) — a call whose arguments reference an outer
19173            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19174            // outer row, so it rides the lateral channel. Same rule the unnest
19175            // arm uses.
19176            let tref = self.parse_table_fn_ref()?;
19177            let correlated = tref
19178                .table_fn_call
19179                .as_deref()
19180                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19181            return Ok(if correlated {
19182                Self::wrap_correlated_srf(tref)
19183            } else {
19184                tref
19185            });
19186        }
19187        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19188        // [, step])` set-returning source. Same shape as unnest:
19189        // detect at the head, parse the comma-separated arg list,
19190        // dispatch downstream through the engine's set-returning
19191        // path. Supports integer triplets (mailrs's `WITH row_no AS
19192        // (SELECT * FROM generate_series(1, N))` pattern) and
19193        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19194        // date-range iteration pattern, which pre-3.10 had no
19195        // direct equivalent in SPG).
19196        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19197            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19198        {
19199            self.advance(); // generate_series
19200            self.advance(); // (
19201            let mut args: Vec<Expr> = Vec::new();
19202            loop {
19203                args.push(self.parse_expr(0)?);
19204                if matches!(self.peek(), Token::Comma) {
19205                    self.advance();
19206                    continue;
19207                }
19208                break;
19209            }
19210            if !matches!(self.peek(), Token::RParen) {
19211                return Err(self.err(alloc::format!(
19212                    "expected ')' after generate_series() arguments, got {:?}",
19213                    self.peek()
19214                )));
19215            }
19216            self.advance();
19217            if args.len() < 2 || args.len() > 3 {
19218                return Err(self.err(alloc::format!(
19219                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19220                    args.len()
19221                )));
19222            }
19223            let with_ordinality = self.absorb_with_ordinality();
19224            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19225            let name = alias_ident
19226                .clone()
19227                .unwrap_or_else(|| "generate_series".to_string());
19228            let correlated = args.iter().any(Self::expr_has_any_column);
19229            let tref = TableRef {
19230                name,
19231                alias: alias_ident,
19232                only: false,
19233                as_of_segment: None,
19234                unnest_expr: None,
19235                unnest_column_aliases: column_aliases,
19236                with_ordinality,
19237                generate_series_args: Some(args),
19238                lateral_subquery: None,
19239                jsonb_each_text_arg: None,
19240                table_fn_call: None,
19241                rows_from: None,
19242                json_table: None,
19243                scalar_fn_item: false,
19244            };
19245            return Ok(if correlated {
19246                Self::wrap_correlated_srf(tref)
19247            } else {
19248                tref
19249            });
19250        }
19251        // v7.16.2 — preserve information_schema / pg_catalog
19252        // qualifiers (mailrs round-10 A.3). The generic
19253        // `expect_ident_like` strip silently drops the schema;
19254        // we want the engine to recognise these PG meta tables
19255        // and synthesise rows from the live catalog. Produce a
19256        // synthetic name (`__spg_info_columns` etc.) so the
19257        // engine's SELECT-side router can dispatch without
19258        // clashing with any user-defined `columns` table.
19259        let name = if let Some(synth) = self.try_peek_meta_qualified() {
19260            synth
19261        } else if let Some(synth) = self.try_peek_meta_bare() {
19262            synth
19263        } else {
19264            self.expect_ident_like()?
19265        };
19266        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19267        // time-travel clause. Parse BEFORE the alias so the
19268        // alias can still ride at the tail (`tbl AS OF SEGMENT
19269        // '5' alias`). `AS` is a reserved keyword token, while
19270        // `OF` and `SEGMENT` are bare idents.
19271        let as_of_segment = if matches!(self.peek(), Token::As)
19272            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19273        {
19274            self.advance(); // AS
19275            self.advance(); // OF
19276            let kw = match self.peek().clone() {
19277                Token::Ident(s) | Token::QuotedIdent(s) => s,
19278                other => {
19279                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19280                }
19281            };
19282            if !kw.eq_ignore_ascii_case("segment") {
19283                return Err(self.err(format!(
19284                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19285                )));
19286            }
19287            self.advance();
19288            // Segment id literal — accept either a string or
19289            // integer for operator ergonomics.
19290            let id = match self.advance() {
19291                Token::String(s) => s
19292                    .parse::<u32>()
19293                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19294                Token::Integer(n) => u32::try_from(n)
19295                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19296                other => {
19297                    return Err(self.err(format!(
19298                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
19299                    )));
19300                }
19301            };
19302            Some(id)
19303        } else {
19304            None
19305        };
19306        // TABLESAMPLE is not a reserved token — keep the bare-ident
19307        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19308        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19309        {
19310            None
19311        } else {
19312            self.parse_optional_alias()?
19313        };
19314        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19315        // (PG grammar). BERNOULLI lowers to a per-row
19316        // `random() < p/100` conjunct on the enclosing SELECT's
19317        // WHERE — exact row-level Bernoulli semantics. SYSTEM
19318        // shares the lowering: SPG has no page structure to
19319        // sample, and the row-level form returns the same expected
19320        // fraction. REPEATABLE(seed) promises a deterministic
19321        // sample SPG cannot honour yet — honest error rather than
19322        // a silently ignored seed.
19323        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19324            self.advance();
19325            let method = self.expect_ident_like()?;
19326            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19327                return Err(self.err(alloc::format!(
19328                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19329                )));
19330            }
19331            if !matches!(self.peek(), Token::LParen) {
19332                return Err(self.err(alloc::format!(
19333                    "expected '(' after TABLESAMPLE {}, got {:?}",
19334                    method.to_ascii_uppercase(),
19335                    self.peek()
19336                )));
19337            }
19338            self.advance();
19339            let percent = self.parse_expr(0)?;
19340            if !matches!(self.peek(), Token::RParen) {
19341                return Err(self.err(alloc::format!(
19342                    "expected ')' after TABLESAMPLE percentage, got {:?}",
19343                    self.peek()
19344                )));
19345            }
19346            self.advance();
19347            // REPEATABLE(seed) → a deterministic per-row draw seeded by
19348            // `seed`, so the sample is stable across repeats and rescans.
19349            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19350            let mut sample_seed: Option<Expr> = None;
19351            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19352                self.advance();
19353                if !matches!(self.peek(), Token::LParen) {
19354                    return Err(self.err(alloc::format!(
19355                        "expected '(' after REPEATABLE, got {:?}",
19356                        self.peek()
19357                    )));
19358                }
19359                self.advance();
19360                let seed = self.parse_expr(0)?;
19361                if !matches!(self.peek(), Token::RParen) {
19362                    return Err(self.err(alloc::format!(
19363                        "expected ')' after REPEATABLE seed, got {:?}",
19364                        self.peek()
19365                    )));
19366                }
19367                self.advance();
19368                sample_seed = Some(seed);
19369            }
19370            let draw = match sample_seed {
19371                Some(seed) => Expr::FunctionCall {
19372                    name: "__tsm_fract".to_string(),
19373                    args: alloc::vec![seed],
19374                },
19375                None => Expr::FunctionCall {
19376                    name: "random".to_string(),
19377                    args: Vec::new(),
19378                },
19379            };
19380            self.pending_sample_preds.push(Expr::Binary {
19381                lhs: Box::new(draw),
19382                op: crate::ast::BinOp::Lt,
19383                rhs: Box::new(Expr::Binary {
19384                    lhs: Box::new(percent),
19385                    op: crate::ast::BinOp::Div,
19386                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19387                }),
19388            });
19389        }
19390        Ok(TableRef {
19391            name,
19392            alias,
19393            only,
19394            as_of_segment,
19395            unnest_expr: None,
19396            unnest_column_aliases: Vec::new(),
19397            with_ordinality: false,
19398            generate_series_args: None,
19399            lateral_subquery: None,
19400            jsonb_each_text_arg: None,
19401            table_fn_call: None,
19402            rows_from: None,
19403            json_table: None,
19404            scalar_fn_item: false,
19405        })
19406    }
19407
19408    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19409    /// but also accepts `AS alias(col [, col, …])` — the
19410    /// PG-standard table-function column-list form. The column
19411    /// list is only honoured when paired with `UNNEST(...)` in
19412    /// the parent; other call sites currently discard it.
19413    /// True when the expression tree contains a qualified column
19414    /// reference (`t.col`) — the syntactic marker that an SRF
19415    /// argument correlates with a preceding FROM item.
19416    fn expr_has_qualified_column(e: &Expr) -> bool {
19417        match e {
19418            Expr::Column(c) => c.qualifier.is_some(),
19419            Expr::Binary { lhs, rhs, .. } => {
19420                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19421            }
19422            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19423            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19424            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19425            Expr::Case {
19426                operand,
19427                branches,
19428                else_branch,
19429            } => {
19430                operand
19431                    .as_deref()
19432                    .is_some_and(Self::expr_has_qualified_column)
19433                    || branches.iter().any(|(w, t)| {
19434                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19435                    })
19436                    || else_branch
19437                        .as_deref()
19438                        .is_some_and(Self::expr_has_qualified_column)
19439            }
19440            _ => false,
19441        }
19442    }
19443
19444    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19445    /// counts a bare (unqualified) column. A set-returning function has no
19446    /// input columns of its own, so ANY column in its arguments is an outer
19447    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19448    fn expr_has_any_column(e: &Expr) -> bool {
19449        match e {
19450            Expr::Column(_) => true,
19451            Expr::Binary { lhs, rhs, .. } => {
19452                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19453            }
19454            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19455            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19456            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19457            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19458            // constructor or subscript fell to the `_ => false` arm, so
19459            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19460            // channel and the eager peer eval answered `column "x" does
19461            // not exist` (the substitution walker already recurses both
19462            // shapes; only this detector was blind to them).
19463            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19464            Expr::ArraySubscript { target, index } => {
19465                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19466            }
19467            Expr::Case {
19468                operand,
19469                branches,
19470                else_branch,
19471            } => {
19472                operand.as_deref().is_some_and(Self::expr_has_any_column)
19473                    || branches
19474                        .iter()
19475                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19476                    || else_branch
19477                        .as_deref()
19478                        .is_some_and(Self::expr_has_any_column)
19479            }
19480            _ => false,
19481        }
19482    }
19483
19484    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19485    /// `generate_series(1, t.n)`) into the lateral_subquery
19486    /// channel: `SELECT * FROM <srf>` executes per outer row with
19487    /// outer references substituted (v7.37.43-T4.5 machinery).
19488    /// Uncorrelated SRFs stay on their plain channels.
19489    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19490        let name = srf.name.clone();
19491        let alias = srf.alias.clone();
19492        let inner = crate::ast::SelectStatement {
19493            locking: None,
19494            ctes: Vec::new(),
19495            distinct: false,
19496            distinct_on: Vec::new(),
19497            items: alloc::vec![crate::ast::SelectItem::Wildcard],
19498            from: Some(crate::ast::FromClause {
19499                primary: srf,
19500                joins: Vec::new(),
19501            }),
19502            where_: None,
19503            group_by: None,
19504            group_by_all: false,
19505            having: None,
19506            unions: Vec::new(),
19507            order_by: Vec::new(),
19508            limit: None,
19509            offset: None,
19510            limit_with_ties: false,
19511            window_check_exprs: Vec::new(),
19512        };
19513        TableRef {
19514            name,
19515            alias,
19516            only: false,
19517            as_of_segment: None,
19518            unnest_expr: None,
19519            unnest_column_aliases: Vec::new(),
19520            with_ordinality: false,
19521            generate_series_args: None,
19522            lateral_subquery: Some(Box::new(inner)),
19523            jsonb_each_text_arg: None,
19524            table_fn_call: None,
19525            rows_from: None,
19526            json_table: None,
19527            scalar_fn_item: false,
19528        }
19529    }
19530
19531    /// True when the expression tree contains an unresolved
19532    /// `OVER w` marker (see parse_over_clause).
19533    fn expr_has_named_window(e: &Expr) -> bool {
19534        match e {
19535            Expr::WindowFunction { partition_by, .. } => matches!(
19536                partition_by.as_slice(),
19537                [Expr::Column(c)] if matches!(
19538                    c.qualifier.as_deref(),
19539                    Some("__named_window__") | Some("__named_window_ref__")
19540                )
19541            ),
19542            Expr::Binary { lhs, rhs, .. } => {
19543                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19544            }
19545            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19546            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19547            Expr::Case {
19548                operand,
19549                branches,
19550                else_branch,
19551            } => {
19552                operand.as_deref().is_some_and(Self::expr_has_named_window)
19553                    || branches.iter().any(|(w, t)| {
19554                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19555                    })
19556                    || else_branch
19557                        .as_deref()
19558                        .is_some_and(Self::expr_has_named_window)
19559            }
19560            _ => false,
19561        }
19562    }
19563
19564    /// v7.39 (round 705) — the NAMES the expression references through the
19565    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19566    /// definitions nothing referenced. Traversal mirrors
19567    /// `expr_has_named_window` above.
19568    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19569        match e {
19570            Expr::WindowFunction { partition_by, .. } => {
19571                if let [Expr::Column(c)] = partition_by.as_slice()
19572                    && matches!(
19573                        c.qualifier.as_deref(),
19574                        Some("__named_window__") | Some("__named_window_ref__")
19575                    )
19576                {
19577                    into.push(c.name.clone());
19578                }
19579            }
19580            Expr::Binary { lhs, rhs, .. } => {
19581                Self::collect_named_window_refs(lhs, into);
19582                Self::collect_named_window_refs(rhs, into);
19583            }
19584            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19585                Self::collect_named_window_refs(expr, into);
19586            }
19587            Expr::FunctionCall { args, .. } => {
19588                for a in args {
19589                    Self::collect_named_window_refs(a, into);
19590                }
19591            }
19592            Expr::Case {
19593                operand,
19594                branches,
19595                else_branch,
19596            } => {
19597                if let Some(o) = operand.as_deref() {
19598                    Self::collect_named_window_refs(o, into);
19599                }
19600                for (w, t) in branches {
19601                    Self::collect_named_window_refs(w, into);
19602                    Self::collect_named_window_refs(t, into);
19603                }
19604                if let Some(eb) = else_branch.as_deref() {
19605                    Self::collect_named_window_refs(eb, into);
19606                }
19607            }
19608            _ => {}
19609        }
19610    }
19611
19612    /// Inline named-window definitions into the `OVER w` markers.
19613    /// An unknown name errors (PG: window "w" does not exist).
19614    #[allow(clippy::type_complexity)]
19615    fn substitute_named_windows(
19616        e: &mut Expr,
19617        defs: &[(
19618            String,
19619            (
19620                Vec<Expr>,
19621                Vec<(Expr, bool, Option<bool>)>,
19622                Option<WindowFrame>,
19623            ),
19624        )],
19625    ) -> Result<(), String> {
19626        match e {
19627            Expr::WindowFunction {
19628                partition_by,
19629                order_by,
19630                frame,
19631                ..
19632            } => {
19633                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
19634                // from the bare `OVER w1` (a plain reference).
19635                let named = match partition_by.as_slice() {
19636                    [Expr::Column(c)] => match c.qualifier.as_deref() {
19637                        Some("__named_window__") => Some((c.name.clone(), false)),
19638                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
19639                        _ => None,
19640                    },
19641                    _ => None,
19642                };
19643                if let Some((wname, is_copy)) = named {
19644                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
19645                    else {
19646                        return Err(alloc::format!("window {wname:?} does not exist"));
19647                    };
19648                    if !is_copy {
19649                        *partition_by = def.0.clone();
19650                        *order_by = def.1.clone();
19651                        *frame = def.2.clone();
19652                        return Ok(());
19653                    }
19654                    // v7.39 (round 229) — PG's copy rules, probed against
19655                    // 18.4: a copy inherits the partitioning, may supply an
19656                    // ordering only when the base has none, and may not copy
19657                    // a base that already carries a frame (its own frame
19658                    // would be ambiguous with the inherited one).
19659                    if !def.1.is_empty() && !order_by.is_empty() {
19660                        return Err(alloc::format!(
19661                            "cannot override ORDER BY clause of window \"{wname}\""
19662                        ));
19663                    }
19664                    if def.2.is_some() {
19665                        return Err(alloc::format!(
19666                            "cannot copy window \"{wname}\" because it has a frame clause"
19667                        ));
19668                    }
19669                    *partition_by = def.0.clone();
19670                    if order_by.is_empty() {
19671                        *order_by = def.1.clone();
19672                    }
19673                }
19674                Ok(())
19675            }
19676            Expr::Binary { lhs, rhs, .. } => {
19677                Self::substitute_named_windows(lhs, defs)?;
19678                Self::substitute_named_windows(rhs, defs)
19679            }
19680            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19681                Self::substitute_named_windows(expr, defs)
19682            }
19683            Expr::FunctionCall { args, .. } => {
19684                for a in args {
19685                    Self::substitute_named_windows(a, defs)?;
19686                }
19687                Ok(())
19688            }
19689            Expr::Case {
19690                operand,
19691                branches,
19692                else_branch,
19693            } => {
19694                if let Some(op) = operand {
19695                    Self::substitute_named_windows(op, defs)?;
19696                }
19697                for (w, t) in branches {
19698                    Self::substitute_named_windows(w, defs)?;
19699                    Self::substitute_named_windows(t, defs)?;
19700                }
19701                if let Some(el) = else_branch {
19702                    Self::substitute_named_windows(el, defs)?;
19703                }
19704                Ok(())
19705            }
19706            _ => Ok(()),
19707        }
19708    }
19709
19710    /// SQL-standard `TABLE name` shorthand — builds the equivalent
19711    /// `SELECT * FROM name` head. Callers own set-op chain / tail
19712    /// composition.
19713    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
19714        debug_assert!(matches!(self.peek(), Token::Table));
19715        self.advance(); // TABLE
19716        let tname = self.expect_ident_like()?;
19717        Ok(SelectStatement {
19718            locking: None,
19719            ctes: Vec::new(),
19720            distinct: false,
19721            distinct_on: Vec::new(),
19722            items: alloc::vec![SelectItem::Wildcard],
19723            from: Some(FromClause {
19724                primary: TableRef {
19725                    name: tname,
19726                    alias: None,
19727                    only: false,
19728                    as_of_segment: None,
19729                    unnest_expr: None,
19730                    unnest_column_aliases: Vec::new(),
19731                    with_ordinality: false,
19732                    generate_series_args: None,
19733                    lateral_subquery: None,
19734                    jsonb_each_text_arg: None,
19735                    table_fn_call: None,
19736                    rows_from: None,
19737                    json_table: None,
19738                    scalar_fn_item: false,
19739                },
19740                joins: Vec::new(),
19741            }),
19742            where_: None,
19743            group_by: None,
19744            group_by_all: false,
19745            having: None,
19746            unions: Vec::new(),
19747            order_by: Vec::new(),
19748            limit: None,
19749            offset: None,
19750            limit_with_ties: false,
19751            window_check_exprs: Vec::new(),
19752        })
19753    }
19754
19755    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
19756    /// variants) → a derived table that reads each declared column out of
19757    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
19758    /// `jsonb_array_elements(J)` (one row per element, column `value`);
19759    /// the scalar *record form projects a single row straight off `J`.
19760    /// Rides the existing lateral-subquery channel, so no new executor or
19761    /// AST is needed.
19762    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
19763        use crate::ast::{
19764            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
19765        };
19766        let fn_name = match self.peek() {
19767            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19768            _ => unreachable!("caller guarded is_json_to_record_name"),
19769        };
19770        self.advance(); // fn name
19771        self.advance(); // (
19772        let mut arg = self.parse_expr(0)?;
19773        // populate_record(base, json): the base only carries the record
19774        // type here — the JSON argument is the second expression.
19775        let mut base: Option<Expr> = None;
19776        if matches!(self.peek(), Token::Comma) {
19777            self.advance();
19778            base = Some(arg);
19779            arg = self.parse_expr(0)?;
19780        }
19781        if !matches!(self.peek(), Token::RParen) {
19782            return Err(self.err(alloc::format!(
19783                "expected ')' after {fn_name}() argument, got {:?}",
19784                self.peek()
19785            )));
19786        }
19787        self.advance(); // )
19788        let is_set = fn_name.ends_with("recordset");
19789        // `[AS] alias ( col type [, …] )` column-definition list.
19790        if matches!(self.peek(), Token::As) {
19791            self.advance();
19792        }
19793        let alias_opt = match self.peek() {
19794            Token::Ident(s) | Token::QuotedIdent(s) => {
19795                let a = s.clone();
19796                self.advance();
19797                Some(a)
19798            }
19799            _ => None,
19800        };
19801        // v7.39 (read01 round 76) — the populate family's canonical PG
19802        // spelling carries no column list at all: the row shape comes from
19803        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
19804        // j)`). The parser has no catalog, so hand the two arguments to the
19805        // engine's table-function channel, which does. Only `*_to_record*`
19806        // (whose base is bare `record`) genuinely requires the list.
19807        if !matches!(self.peek(), Token::LParen) {
19808            if let Some(base_expr) = base {
19809                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
19810                return Ok(TableRef {
19811                    name: alias.clone(),
19812                    alias: Some(alias),
19813                    only: false,
19814                    as_of_segment: None,
19815                    unnest_expr: None,
19816                    unnest_column_aliases: Vec::new(),
19817                    with_ordinality: false,
19818                    generate_series_args: None,
19819                    lateral_subquery: None,
19820                    jsonb_each_text_arg: None,
19821                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
19822                    rows_from: None,
19823                    json_table: None,
19824                    scalar_fn_item: false,
19825                });
19826            }
19827            return Err(self.err(alloc::format!(
19828                "expected '(' to start the {fn_name} column-definition list, got {:?}",
19829                self.peek()
19830            )));
19831        }
19832        let Some(alias) = alias_opt else {
19833            return Err(self.err(alloc::format!(
19834                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
19835            )));
19836        };
19837        self.advance(); // (
19838        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
19839        loop {
19840            let col = self.expect_ident_like()?;
19841            let ty = self.parse_cast_target()?;
19842            coldefs.push((col, ty));
19843            if matches!(self.peek(), Token::Comma) {
19844                self.advance();
19845                continue;
19846            }
19847            if matches!(self.peek(), Token::RParen) {
19848                self.advance();
19849                break;
19850            }
19851            return Err(self.err(alloc::format!(
19852                "expected ',' or ')' in {fn_name} column list, got {:?}",
19853                self.peek()
19854            )));
19855        }
19856        if coldefs.is_empty() {
19857            return Err(self.err(alloc::format!(
19858                "{fn_name} column-definition list must declare at least one column"
19859            )));
19860        }
19861        // Per column: (base ->> 'col')::type AS col. The base is the
19862        // per-element `value` column for the *set form, or the argument
19863        // itself for the scalar record form.
19864        let items: Vec<SelectItem> = coldefs
19865            .into_iter()
19866            .map(|(col, ty)| {
19867                let base = if is_set {
19868                    Expr::Column(ColumnName {
19869                        qualifier: None,
19870                        name: "value".to_string(),
19871                    })
19872                } else {
19873                    arg.clone()
19874                };
19875                SelectItem::Expr {
19876                    expr: Expr::Cast {
19877                        expr: Box::new(Expr::Binary {
19878                            lhs: Box::new(base),
19879                            op: BinOp::JsonGetText,
19880                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
19881                        }),
19882                        target: ty,
19883                    },
19884                    alias: Some(col),
19885                }
19886            })
19887            .collect();
19888        let from = if is_set {
19889            let elem_fn = if fn_name.starts_with("jsonb") {
19890                "jsonb_array_elements"
19891            } else {
19892                "json_array_elements"
19893            };
19894            Some(FromClause {
19895                primary: TableRef {
19896                    name: "value".to_string(),
19897                    alias: None,
19898                    only: false,
19899                    as_of_segment: None,
19900                    unnest_expr: Some(Box::new(Expr::FunctionCall {
19901                        name: elem_fn.to_string(),
19902                        args: alloc::vec![arg],
19903                    })),
19904                    unnest_column_aliases: alloc::vec!["value".to_string()],
19905                    with_ordinality: false,
19906                    generate_series_args: None,
19907                    lateral_subquery: None,
19908                    jsonb_each_text_arg: None,
19909                    table_fn_call: None,
19910                    rows_from: None,
19911                    json_table: None,
19912                    scalar_fn_item: false,
19913                },
19914                joins: Vec::new(),
19915            })
19916        } else {
19917            None
19918        };
19919        let inner = SelectStatement {
19920            locking: None,
19921            ctes: Vec::new(),
19922            distinct: false,
19923            distinct_on: Vec::new(),
19924            items,
19925            from,
19926            where_: None,
19927            group_by: None,
19928            group_by_all: false,
19929            having: None,
19930            unions: Vec::new(),
19931            order_by: Vec::new(),
19932            limit: None,
19933            offset: None,
19934            limit_with_ties: false,
19935            window_check_exprs: Vec::new(),
19936        };
19937        Ok(TableRef {
19938            name: alias.clone(),
19939            alias: Some(alias),
19940            only: false,
19941            as_of_segment: None,
19942            unnest_expr: None,
19943            unnest_column_aliases: Vec::new(),
19944            with_ordinality: false,
19945            generate_series_args: None,
19946            lateral_subquery: Some(Box::new(inner)),
19947            jsonb_each_text_arg: None,
19948            table_fn_call: None,
19949            rows_from: None,
19950            json_table: None,
19951            scalar_fn_item: false,
19952        })
19953    }
19954
19955    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
19956    /// Returns true when the clause was present. `WITH` alone (a
19957    /// CTE can never start here) is not enough — the ORDINALITY
19958    /// ident must follow, so a stray WITH still errors downstream.
19959    fn absorb_with_ordinality(&mut self) -> bool {
19960        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
19961            && matches!(self.tokens.get(self.pos + 1),
19962                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
19963        {
19964            self.advance();
19965            self.advance();
19966            true
19967        } else {
19968            false
19969        }
19970    }
19971
19972    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
19973    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
19974    /// Out-of-line: the caller sits on the FROM recursion chain.
19975    #[inline(never)]
19976    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
19977        let fn_name = match self.advance() {
19978            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19979            _ => unreachable!("caller peeked an ident"),
19980        };
19981        self.advance(); // (
19982        let mut args: Vec<Expr> = Vec::new();
19983        if !matches!(self.peek(), Token::RParen) {
19984            loop {
19985                args.push(self.parse_expr(0)?);
19986                if matches!(self.peek(), Token::Comma) {
19987                    self.advance();
19988                    continue;
19989                }
19990                break;
19991            }
19992        }
19993        if !matches!(self.peek(), Token::RParen) {
19994            return Err(self.err(alloc::format!(
19995                "expected ')' after {fn_name}() arguments, got {:?}",
19996                self.peek()
19997            )));
19998        }
19999        self.advance();
20000        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20001        // counter column rides after the function's own, and the alias list
20002        // names it.
20003        let with_ordinality = self.absorb_with_ordinality();
20004        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20005        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20006        Ok(TableRef {
20007            name,
20008            alias: alias_ident,
20009            only: false,
20010            as_of_segment: None,
20011            unnest_expr: None,
20012            unnest_column_aliases,
20013            with_ordinality,
20014            generate_series_args: None,
20015            lateral_subquery: None,
20016            jsonb_each_text_arg: None,
20017            table_fn_call: Some(Box::new((fn_name, args))),
20018            rows_from: None,
20019            json_table: None,
20020            scalar_fn_item: false,
20021        })
20022    }
20023
20024    /// v7.39 (round 205, JSON_TABLE) — parse
20025    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20026    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20027    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20028    #[inline(never)]
20029    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20030        self.advance(); // json_table
20031        self.advance(); // (
20032        let doc = Box::new(self.parse_expr(0)?);
20033        self.expect_comma_json_table()?;
20034        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20035        // Optional `PASSING <expr> AS <name> [, …]`.
20036        let mut passing: Vec<(String, Expr)> = Vec::new();
20037        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20038            self.advance();
20039            loop {
20040                let e = self.parse_expr(0)?;
20041                if !matches!(self.peek(), Token::As) {
20042                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20043                }
20044                self.advance();
20045                let vname = match self.advance() {
20046                    Token::Ident(s) | Token::QuotedIdent(s) => s,
20047                    other => {
20048                        return Err(self.err(alloc::format!(
20049                            "expected PASSING variable name, got {other:?}"
20050                        )));
20051                    }
20052                };
20053                passing.push((vname, e));
20054                if matches!(self.peek(), Token::Comma) {
20055                    self.advance();
20056                    continue;
20057                }
20058                break;
20059            }
20060        }
20061        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20062            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20063        }
20064        self.advance();
20065        let columns = self.parse_json_table_columns()?;
20066        if !matches!(self.peek(), Token::RParen) {
20067            return Err(self.err(alloc::format!(
20068                "expected ')' to close JSON_TABLE, got {:?}",
20069                self.peek()
20070            )));
20071        }
20072        self.advance();
20073        let alias_ident = self.parse_optional_alias()?;
20074        let name = alias_ident
20075            .clone()
20076            .unwrap_or_else(|| String::from("json_table"));
20077        Ok(TableRef {
20078            name,
20079            alias: alias_ident,
20080            only: false,
20081            as_of_segment: None,
20082            unnest_expr: None,
20083            unnest_column_aliases: Vec::new(),
20084            with_ordinality: false,
20085            generate_series_args: None,
20086            lateral_subquery: None,
20087            jsonb_each_text_arg: None,
20088            table_fn_call: None,
20089            rows_from: None,
20090            json_table: Some(Box::new(crate::ast::JsonTable {
20091                doc,
20092                row_path,
20093                columns,
20094                passing,
20095            })),
20096            scalar_fn_item: false,
20097        })
20098    }
20099
20100    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20101        if !matches!(self.peek(), Token::Comma) {
20102            return Err(self.err(alloc::format!(
20103                "expected ',' after JSON_TABLE document, got {:?}",
20104                self.peek()
20105            )));
20106        }
20107        self.advance();
20108        Ok(())
20109    }
20110
20111    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20112        match self.advance() {
20113            Token::String(s) => Ok(s),
20114            other => Err(self.err(alloc::format!(
20115                "expected {what} string literal, got {other:?}"
20116            ))),
20117        }
20118    }
20119
20120    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20121    #[inline(never)]
20122    fn parse_json_table_columns(
20123        &mut self,
20124    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20125        if !matches!(self.peek(), Token::LParen) {
20126            return Err(self.err("expected '(' after COLUMNS".into()));
20127        }
20128        self.advance();
20129        let mut cols = Vec::new();
20130        loop {
20131            cols.push(self.parse_json_table_one_column()?);
20132            if matches!(self.peek(), Token::Comma) {
20133                self.advance();
20134                continue;
20135            }
20136            break;
20137        }
20138        if !matches!(self.peek(), Token::RParen) {
20139            return Err(self.err(alloc::format!(
20140                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20141                self.peek()
20142            )));
20143        }
20144        self.advance();
20145        Ok(cols)
20146    }
20147
20148    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20149        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20150        // NESTED [PATH] '<p>' COLUMNS (...)
20151        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20152            self.advance();
20153            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20154                self.advance();
20155            }
20156            let path = self.parse_json_string_literal("NESTED PATH")?;
20157            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20158                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20159            }
20160            self.advance();
20161            let columns = self.parse_json_table_columns()?;
20162            return Ok(JsonTableColumn::Nested { path, columns });
20163        }
20164        // <name> ...
20165        let name = match self.advance() {
20166            Token::Ident(s) | Token::QuotedIdent(s) => s,
20167            other => {
20168                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20169            }
20170        };
20171        // <name> FOR ORDINALITY
20172        if matches!(self.peek(), Token::For) {
20173            self.advance();
20174            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20175                return Err(self.err("expected ORDINALITY after FOR".into()));
20176            }
20177            self.advance();
20178            return Ok(JsonTableColumn::Ordinality { name });
20179        }
20180        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20181        let ty = self.parse_column_type_name()?;
20182        let mut format_json = false;
20183        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20184            self.advance();
20185            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20186                return Err(self.err("expected JSON after FORMAT".into()));
20187            }
20188            self.advance();
20189            format_json = true;
20190        }
20191        let mut exists = false;
20192        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20193            self.advance();
20194            exists = true;
20195        }
20196        let mut path = alloc::format!("$.{name}");
20197        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20198            self.advance();
20199            path = self.parse_json_string_literal("column PATH")?;
20200        }
20201        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20202            // `FORMAT JSON` after PATH (alternate placement).
20203            self.advance();
20204            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20205                self.advance();
20206            }
20207            format_json = true;
20208        }
20209        let mut wrapper = false;
20210        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20211            self.advance();
20212            // optional CONDITIONAL/UNCONDITIONAL
20213            if matches!(self.peek(), Token::Ident(s)
20214                if s.eq_ignore_ascii_case("unconditional")
20215                    || s.eq_ignore_ascii_case("conditional"))
20216            {
20217                self.advance();
20218            }
20219            if !matches!(self.peek(), Token::Ident(s)
20220                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20221            {
20222                return Err(self.err("expected WRAPPER after WITH".into()));
20223            }
20224            self.advance();
20225            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20226            if matches!(self.peek(), Token::Ident(s)
20227                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20228            {
20229                self.advance();
20230            }
20231            wrapper = true;
20232        }
20233        // ON EMPTY / ON ERROR clauses (two, in any order).
20234        let mut on_empty = JsonTableOnBehavior::Null;
20235        let mut on_error = JsonTableOnBehavior::Null;
20236        for _ in 0..2 {
20237            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20238            {
20239                self.advance();
20240                Some(JsonTableOnBehavior::Error)
20241            } else if matches!(self.peek(), Token::Null) {
20242                self.advance();
20243                Some(JsonTableOnBehavior::Null)
20244            } else if matches!(self.peek(), Token::Default) {
20245                self.advance();
20246                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20247            } else {
20248                None
20249            };
20250            let Some(behavior) = behavior else { break };
20251            // `ON {EMPTY|ERROR}`
20252            if !matches!(self.peek(), Token::On) {
20253                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20254            }
20255            self.advance();
20256            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20257                self.advance();
20258                on_empty = behavior;
20259            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20260                self.advance();
20261                on_error = behavior;
20262            } else {
20263                return Err(self.err("expected EMPTY or ERROR after ON".into()));
20264            }
20265        }
20266        Ok(JsonTableColumn::Regular {
20267            name,
20268            ty,
20269            path,
20270            exists,
20271            format_json,
20272            wrapper,
20273            on_empty,
20274            on_error,
20275        })
20276    }
20277
20278    fn parse_optional_alias_with_columns(
20279        &mut self,
20280    ) -> Result<(Option<String>, Vec<String>), ParseError> {
20281        let alias = self.parse_optional_alias()?;
20282        if alias.is_none() {
20283            return Ok((None, Vec::new()));
20284        }
20285        let mut cols: Vec<String> = Vec::new();
20286        if matches!(self.peek(), Token::LParen) {
20287            self.advance();
20288            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20289                self.advance();
20290                cols.push(s);
20291                if matches!(self.peek(), Token::Comma) {
20292                    self.advance();
20293                    continue;
20294                }
20295                break;
20296            }
20297            if matches!(self.peek(), Token::RParen) {
20298                self.advance();
20299            }
20300        }
20301        Ok((alias, cols))
20302    }
20303
20304    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20305    /// whose keyword token was already consumed and whose `(` is the
20306    /// current token. Factored out of `parse_atom` (and marked
20307    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20308    /// recursive `parse_atom` frame — inlining them there enlarges the
20309    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20310    /// against, risking an overflow before the budget triggers.
20311    #[inline(never)]
20312    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20313        self.advance(); // (
20314        let mut args = Vec::new();
20315        if !matches!(self.peek(), Token::RParen) {
20316            loop {
20317                args.push(self.parse_expr(0)?);
20318                match self.peek() {
20319                    Token::Comma => {
20320                        self.advance();
20321                    }
20322                    Token::RParen => break,
20323                    other => {
20324                        return Err(self.err(alloc::format!(
20325                            "expected ',' or ')' in {name}() args, got {other:?}"
20326                        )));
20327                    }
20328                }
20329            }
20330        }
20331        self.advance(); // )
20332        Ok(Expr::FunctionCall {
20333            name: name.into(),
20334            args,
20335        })
20336    }
20337
20338    /// FROM-clause: a primary table reference plus zero-or-more joined
20339    /// peers expressed via either `, <table>` (cross-product, no ON) or
20340    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20341    /// v1.10 keeps the join list flat (left-associative nested-loop
20342    /// semantics).
20343    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20344        let primary = self.parse_table_ref()?;
20345        let primary_qual = primary
20346            .alias
20347            .clone()
20348            .unwrap_or_else(|| primary.name.clone());
20349        let joins = self.parse_from_joins(&primary_qual)?;
20350        Ok(FromClause { primary, joins })
20351    }
20352
20353    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20354    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20355    /// SAME grammar after its target table has already been consumed.
20356    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20357    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20358    /// be parsed forward, once.)
20359    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20360    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20361    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20362    /// desugaring, which needs a name for the left side of each equality.
20363    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20364        let mut joins = Vec::new();
20365        loop {
20366            // `, <table>` — cross-product with no ON.
20367            if matches!(self.peek(), Token::Comma) {
20368                self.advance();
20369                let table = self.parse_table_ref()?;
20370                joins.push(FromJoin {
20371                    kind: JoinKind::Cross,
20372                    table,
20373                    on: None,
20374                    using_cols: None,
20375                    natural: false,
20376                });
20377                continue;
20378            }
20379            // v7.37.16 — optional leading `NATURAL` before the join
20380            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20381            // not a lexer keyword (it arrives as a bare Ident), so match
20382            // it case-insensitively here. When present, no ON/USING
20383            // clause is allowed — the common columns are resolved at
20384            // execution time.
20385            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20386            if natural {
20387                self.advance();
20388            }
20389            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20390            // CROSS JOIN, and bare JOIN (defaults to INNER).
20391            let kind =
20392                match self.peek() {
20393                    Token::Inner => {
20394                        self.advance();
20395                        if !matches!(self.peek(), Token::Join) {
20396                            return Err(self
20397                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20398                        }
20399                        self.advance();
20400                        JoinKind::Inner
20401                    }
20402                    Token::Left => {
20403                        self.advance();
20404                        if matches!(self.peek(), Token::Outer) {
20405                            self.advance();
20406                        }
20407                        if !matches!(self.peek(), Token::Join) {
20408                            return Err(self.err(format!(
20409                                "expected JOIN after LEFT [OUTER], got {:?}",
20410                                self.peek()
20411                            )));
20412                        }
20413                        self.advance();
20414                        JoinKind::Left
20415                    }
20416                    Token::Cross => {
20417                        self.advance();
20418                        if !matches!(self.peek(), Token::Join) {
20419                            return Err(self
20420                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20421                        }
20422                        self.advance();
20423                        JoinKind::Cross
20424                    }
20425                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20426                    Token::Right => {
20427                        self.advance();
20428                        if matches!(self.peek(), Token::Outer) {
20429                            self.advance();
20430                        }
20431                        if !matches!(self.peek(), Token::Join) {
20432                            return Err(self.err(format!(
20433                                "expected JOIN after RIGHT [OUTER], got {:?}",
20434                                self.peek()
20435                            )));
20436                        }
20437                        self.advance();
20438                        JoinKind::Right
20439                    }
20440                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20441                    Token::Full => {
20442                        self.advance();
20443                        if matches!(self.peek(), Token::Outer) {
20444                            self.advance();
20445                        }
20446                        if !matches!(self.peek(), Token::Join) {
20447                            return Err(self.err(format!(
20448                                "expected JOIN after FULL [OUTER], got {:?}",
20449                                self.peek()
20450                            )));
20451                        }
20452                        self.advance();
20453                        JoinKind::FullOuter
20454                    }
20455                    Token::Join => {
20456                        self.advance();
20457                        JoinKind::Inner
20458                    }
20459                    _ => break,
20460                };
20461            let table = self.parse_table_ref()?;
20462            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20463            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20464            // where prev_table is the most-recent left-side table
20465            // (the previous join's table if any, else the FROM primary).
20466            // PG semantics around column merging are richer (USING'd
20467            // cols become deduplicated single output columns); for
20468            // sugar purposes the predicate-only form covers the
20469            // baseline corpus shape and chained `… JOIN x USING (k)
20470            // JOIN y USING (k)` calls.
20471            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20472            // common columns resolve at execution time.
20473            if natural {
20474                joins.push(FromJoin {
20475                    kind,
20476                    table,
20477                    on: None,
20478                    using_cols: None,
20479                    natural: true,
20480                });
20481                continue;
20482            }
20483            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20484            // v7.37.16 — capture the USING column list (in addition to
20485            // the ON desugar below) so the executor can perform PG's
20486            // column-merge on the output side.
20487            let mut using_cols: Option<Vec<String>> = None;
20488            let on = if matches!(self.peek(), Token::On) {
20489                self.advance();
20490                Some(self.parse_expr(0)?)
20491            } else if using_match {
20492                self.advance();
20493                if !matches!(self.peek(), Token::LParen) {
20494                    return Err(
20495                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20496                    );
20497                }
20498                self.advance();
20499                let mut cols: Vec<String> = Vec::new();
20500                loop {
20501                    match self.peek().clone() {
20502                        Token::Ident(s) | Token::QuotedIdent(s) => {
20503                            self.advance();
20504                            cols.push(s);
20505                        }
20506                        other => {
20507                            return Err(self.err(format!(
20508                                "expected column name inside USING (…), got {other:?}"
20509                            )));
20510                        }
20511                    }
20512                    match self.peek() {
20513                        Token::Comma => {
20514                            self.advance();
20515                            continue;
20516                        }
20517                        Token::RParen => {
20518                            self.advance();
20519                            break;
20520                        }
20521                        other => {
20522                            return Err(self.err(format!(
20523                                "expected ',' or ')' inside USING (…), got {other:?}"
20524                            )));
20525                        }
20526                    }
20527                }
20528                if cols.is_empty() {
20529                    return Err(self.err("USING (…) requires at least one column".to_string()));
20530                }
20531                using_cols = Some(cols.clone());
20532                // Pick the left-side alias: prev join's table if any,
20533                // else FROM primary. Use alias when present, else
20534                // table name (PG-equivalent qualifier).
20535                let left_qual: String = joins
20536                    .last()
20537                    .map(|j| {
20538                        j.table
20539                            .alias
20540                            .clone()
20541                            .unwrap_or_else(|| j.table.name.clone())
20542                    })
20543                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20544                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20545                let mut iter = cols.into_iter().map(|c| Expr::Binary {
20546                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20547                        qualifier: Some(left_qual.clone()),
20548                        name: c.clone(),
20549                    })),
20550                    op: crate::ast::BinOp::Eq,
20551                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20552                        qualifier: Some(right_qual.clone()),
20553                        name: c,
20554                    })),
20555                });
20556                let first = iter.next().expect("at least one col");
20557                Some(iter.fold(first, |acc, pred| Expr::Binary {
20558                    lhs: alloc::boxed::Box::new(acc),
20559                    op: crate::ast::BinOp::And,
20560                    rhs: alloc::boxed::Box::new(pred),
20561                }))
20562            } else if kind == JoinKind::Cross {
20563                None
20564            } else {
20565                return Err(self.err(format!(
20566                    "expected ON or USING after {:?} JOIN, got {:?}",
20567                    kind,
20568                    self.peek()
20569                )));
20570            };
20571            joins.push(FromJoin {
20572                kind,
20573                table,
20574                on,
20575                using_cols,
20576                natural: false,
20577            });
20578        }
20579        Ok(joins)
20580    }
20581
20582    /// Optional alias after an expression or table:
20583    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20584    /// accepted (PG-style implicit alias). Returns `None` if the next token
20585    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20586    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20587        if matches!(self.peek(), Token::As) {
20588            self.advance();
20589            // v7.39 (round 340, V56) — after AS the next token MUST be an
20590            // identifier. This used to return None and "let the caller
20591            // surface the error on the next expectation", but when AS is
20592            // the LAST token there is no next expectation: `SELECT 1 AS`
20593            // parsed clean and silently dropped the alias. PG rejects it.
20594            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20595                return self.expect_ident_like().map(Some);
20596            }
20597            return Err(self.err(alloc::format!(
20598                "expected an alias after AS, got {:?}",
20599                self.peek()
20600            )));
20601        }
20602        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
20603        // grammar reserves a long list of follow-keywords from the
20604        // alias slot. SPG's bareword approximation: skip a small
20605        // set of idents that would otherwise be swallowed as the
20606        // table alias and break trailing clauses like CREATE
20607        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
20608        // CONFLICT WHERE shapes.
20609        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
20610            if is_alias_stopword(s) {
20611                return Ok(None);
20612            }
20613            return Ok(self.expect_ident_like().ok());
20614        }
20615        Ok(None)
20616    }
20617
20618    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
20619    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
20620        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
20621        // error beats a stack overflow (an overflow aborts the
20622        // embedding host process).
20623        self.enter_nested()?;
20624        let r = self.parse_expr_inner(min_prec);
20625        self.nest_depth -= 1;
20626        r
20627    }
20628
20629    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
20630    /// When the upcoming tokens form one, return the underlying
20631    /// operator token and the position just past the closing paren
20632    /// so the binary loop can dispatch on the plain operator.
20633    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
20634        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
20635            return None;
20636        }
20637        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
20638            return None;
20639        }
20640        let mut i = self.pos + 2;
20641        // Optional schema qualifier (pg_catalog.<op> etc.).
20642        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
20643            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
20644        {
20645            i += 2;
20646        }
20647        let op_tok = self.tokens.get(i)?.clone();
20648        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
20649            return None;
20650        }
20651        Some((i + 2, op_tok))
20652    }
20653
20654    /// PG operator symbols that lower onto function calls in
20655    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
20656    /// family → regexp_like, comparison rung), `^@` (starts_with,
20657    /// comparison rung), `^` (power, tighter than `*`), `#`
20658    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
20659    /// subset of the OR bits so the subtraction never borrows).
20660    fn try_symbol_operator(
20661        &mut self,
20662        lhs: &Expr,
20663        min_prec: u8,
20664    ) -> Result<Option<Expr>, ParseError> {
20665        enum Sym {
20666            Regex { ci: bool, negated: bool },
20667            Like { ci: bool, negated: bool },
20668            StartsWith,
20669            Power,
20670            Xor,
20671            RangeAdjacent,
20672        }
20673        // v7.39 (IS-precedence knife) — the low-precedence postfix
20674        // predicates ride this existing leaf call (zero new frame slots
20675        // on the nesting chain).
20676        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
20677            return Ok(Some(e));
20678        }
20679        let (sym, prec): (Sym, u8) = match self.peek() {
20680            Token::Tilde => (
20681                Sym::Regex {
20682                    ci: false,
20683                    negated: false,
20684                },
20685                5,
20686            ),
20687            Token::TildeStar => (
20688                Sym::Regex {
20689                    ci: true,
20690                    negated: false,
20691                },
20692                5,
20693            ),
20694            Token::NotTilde => (
20695                Sym::Regex {
20696                    ci: false,
20697                    negated: true,
20698                },
20699                5,
20700            ),
20701            Token::NotTildeStar => (
20702                Sym::Regex {
20703                    ci: true,
20704                    negated: true,
20705                },
20706                5,
20707            ),
20708            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
20709            Token::DoubleTilde => (
20710                Sym::Like {
20711                    ci: false,
20712                    negated: false,
20713                },
20714                5,
20715            ),
20716            Token::DoubleTildeStar => (
20717                Sym::Like {
20718                    ci: true,
20719                    negated: false,
20720                },
20721                5,
20722            ),
20723            Token::NotDoubleTilde => (
20724                Sym::Like {
20725                    ci: false,
20726                    negated: true,
20727                },
20728                5,
20729            ),
20730            Token::NotDoubleTildeStar => (
20731                Sym::Like {
20732                    ci: true,
20733                    negated: true,
20734                },
20735                5,
20736            ),
20737            Token::CaretAt => (Sym::StartsWith, 5),
20738            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
20739            // tighter than `* / & |`, which the prec-9 rung preserves —
20740            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
20741            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
20742            Token::Caret => (Sym::Power, 9),
20743            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
20744            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
20745            Token::Hash => (Sym::Xor, 6),
20746            Token::Adjacent => (Sym::RangeAdjacent, 5),
20747            _ => return Ok(None),
20748        };
20749        if prec < min_prec {
20750            return Ok(None);
20751        }
20752        self.advance();
20753        let rhs = self.parse_expr(prec + 1)?;
20754        let out = match sym {
20755            Sym::Regex { ci, negated } => {
20756                let mut args = alloc::vec![lhs.clone(), rhs];
20757                if ci {
20758                    args.push(Expr::Literal(Literal::String(String::from("i"))));
20759                }
20760                maybe_not(
20761                    Expr::FunctionCall {
20762                        name: String::from("regexp_like"),
20763                        args,
20764                    },
20765                    negated,
20766                )
20767            }
20768            Sym::Like { ci, negated } => Expr::Like {
20769                expr: alloc::boxed::Box::new(lhs.clone()),
20770                pattern: alloc::boxed::Box::new(rhs),
20771                negated,
20772                case_insensitive: ci,
20773            },
20774            Sym::StartsWith => Expr::FunctionCall {
20775                name: String::from("starts_with"),
20776                args: alloc::vec![lhs.clone(), rhs],
20777            },
20778            Sym::Power => Expr::FunctionCall {
20779                name: String::from("power"),
20780                args: alloc::vec![lhs.clone(), rhs],
20781            },
20782            // `#` bitwise XOR — a real operator now (was desugared to
20783            // `(a|b)-(a&b)`, algebraically identical for integers but
20784            // undefined for bit strings; the direct op handles both).
20785            Sym::Xor => Expr::Binary {
20786                lhs: Box::new(lhs.clone()),
20787                op: BinOp::BitXor,
20788                rhs: Box::new(rhs),
20789            },
20790            // range `-|-` "is adjacent to" — lowered to a catalog function.
20791            Sym::RangeAdjacent => Expr::FunctionCall {
20792                name: String::from("range_adjacent"),
20793                args: alloc::vec![lhs.clone(), rhs],
20794            },
20795        };
20796        Ok(Some(out))
20797    }
20798
20799    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
20800    /// predicates, moved out of the tight postfix-cast loop: PG binds
20801    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
20802    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
20803    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
20804    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
20805    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
20806    /// when nothing at this position belongs to the family. Out-of-line
20807    /// (`inline(never)`): the caller sits on the per-nesting-level frame
20808    /// chain that MAX_NEST_DEPTH is tuned against.
20809    #[inline(never)]
20810    fn parse_postfix_predicate(
20811        &mut self,
20812        lhs: &Expr,
20813        min_prec: u8,
20814    ) -> Result<Option<Expr>, ParseError> {
20815        // Reached through try_symbol_operator (an existing leaf call of
20816        // the binary loop) so NO new stack slots land on the per-nesting
20817        // frame chain; the lhs clones only when a predicate actually
20818        // consumes it.
20819        match self.peek() {
20820            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
20821            // comparison family rung 5 (each +1 from the pre-XOR ladder).
20822            Token::Is if min_prec <= 4 => {}
20823            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
20824            Token::Not
20825                if min_prec <= 5
20826                    && matches!(
20827                        self.tokens.get(self.pos + 1),
20828                        Some(Token::Between | Token::In | Token::Like)
20829                    ) => {}
20830            Token::Not | Token::Ident(_)
20831                if min_prec <= 5
20832                    && (matches!(self.peek(), Token::Ident(s)
20833                            if s.eq_ignore_ascii_case("ilike")
20834                                || (self.mysql_dialect
20835                                    && (s.eq_ignore_ascii_case("regexp")
20836                                        || s.eq_ignore_ascii_case("rlike")))
20837                                || (s.eq_ignore_ascii_case("similar")
20838                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
20839                        || (matches!(self.peek(), Token::Not)
20840                            && matches!(self.tokens.get(self.pos + 1),
20841                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
20842                                    || (self.mysql_dialect
20843                                        && (s.eq_ignore_ascii_case("regexp")
20844                                            || s.eq_ignore_ascii_case("rlike")))
20845                                    || s.eq_ignore_ascii_case("similar")))) => {}
20846            _ => return Ok(None),
20847        }
20848        let mut expr = lhs.clone();
20849        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
20850        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
20851        if min_prec <= 4 {
20852            if matches!(self.peek(), Token::Is) {
20853                self.advance();
20854                let negated = if matches!(self.peek(), Token::Not) {
20855                    self.advance();
20856                    true
20857                } else {
20858                    false
20859                };
20860                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
20861                // mailrs pg_dump.
20862                if matches!(self.peek(), Token::Distinct) {
20863                    self.advance();
20864                    if !matches!(self.peek(), Token::From) {
20865                        return Err(self.err(format!(
20866                            "expected FROM after IS{} DISTINCT, got {:?}",
20867                            if negated { " NOT" } else { "" },
20868                            self.peek()
20869                        )));
20870                    }
20871                    self.advance();
20872                    // Right-hand side: parse at the same precedence
20873                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
20874                    // groups as `x IS DISTINCT FROM (a + b)`.
20875                    let rhs = self.parse_expr(5)?;
20876                    let op = if negated {
20877                        BinOp::IsNotDistinctFrom
20878                    } else {
20879                        BinOp::IsDistinctFrom
20880                    };
20881                    expr = Expr::Binary {
20882                        op,
20883                        lhs: Box::new(expr),
20884                        rhs: Box::new(rhs),
20885                    };
20886                    {
20887                        return Ok(Some(expr));
20888                    }
20889                }
20890                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
20891                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
20892                // Lowers onto pg_is_json(x, kind); NOT wraps the
20893                // call in a logical negation.
20894                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20895                if s.eq_ignore_ascii_case("json"))
20896                {
20897                    self.advance(); // JSON
20898                    let kind = match self.peek() {
20899                        Token::Ident(s) | Token::QuotedIdent(s)
20900                            if matches!(
20901                                s.to_ascii_lowercase().as_str(),
20902                                "value" | "object" | "array" | "scalar"
20903                            ) =>
20904                        {
20905                            let k = s.to_ascii_lowercase();
20906                            self.advance();
20907                            k
20908                        }
20909                        _ => "value".to_string(),
20910                    };
20911                    let call = Expr::FunctionCall {
20912                        name: "pg_is_json".to_string(),
20913                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
20914                    };
20915                    expr = if negated {
20916                        Expr::Unary {
20917                            op: UnOp::Not,
20918                            expr: Box::new(call),
20919                        }
20920                    } else {
20921                        call
20922                    };
20923                    {
20924                        return Ok(Some(expr));
20925                    }
20926                }
20927                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
20928                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
20929                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
20930                {
20931                    let form_kw = match self.peek() {
20932                        Token::Ident(s) | Token::QuotedIdent(s)
20933                            if matches!(
20934                                s.to_ascii_uppercase().as_str(),
20935                                "NFC" | "NFD" | "NFKC" | "NFKD"
20936                            ) && matches!(
20937                                self.tokens.get(self.pos + 1),
20938                                Some(Token::Ident(n) | Token::QuotedIdent(n))
20939                                    if n.eq_ignore_ascii_case("normalized")
20940                            ) =>
20941                        {
20942                            Some(s.to_ascii_uppercase())
20943                        }
20944                        _ => None,
20945                    };
20946                    let bare_normalized = form_kw.is_none()
20947                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20948                        if s.eq_ignore_ascii_case("normalized"));
20949                    if form_kw.is_some() || bare_normalized {
20950                        if form_kw.is_some() {
20951                            self.advance(); // form keyword
20952                        }
20953                        self.advance(); // NORMALIZED
20954                        let mut args = alloc::vec![expr];
20955                        if let Some(f) = form_kw {
20956                            args.push(Expr::Literal(Literal::String(f)));
20957                        }
20958                        let call = Expr::FunctionCall {
20959                            name: "is_normalized".to_string(),
20960                            args,
20961                        };
20962                        expr = if negated {
20963                            Expr::Unary {
20964                                op: UnOp::Not,
20965                                expr: Box::new(call),
20966                            }
20967                        } else {
20968                            call
20969                        };
20970                        {
20971                            return Ok(Some(expr));
20972                        }
20973                    }
20974                }
20975                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
20976                // three-valued boolean tests. IS TRUE/FALSE never
20977                // return NULL, so they lower to CASE forms whose
20978                // ELSE catches the NULL branch; IS UNKNOWN on a
20979                // boolean is exactly IS NULL.
20980                if matches!(self.peek(), Token::True | Token::False)
20981                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
20982                {
20983                    let tok = self.advance();
20984                    let test = match tok {
20985                        Token::True => Some(true),
20986                        Token::False => Some(false),
20987                        _ => None, // UNKNOWN
20988                    };
20989                    // v7.39 (round 328, V45) — kept as what the user
20990                    // wrote. These used to be lowered here into `CASE` /
20991                    // `IS NULL`; the semantics were right but the AST no
20992                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
20993                    // was echoed back as
20994                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
20995                    expr = Expr::BoolTest {
20996                        expr: Box::new(expr),
20997                        value: test,
20998                        negated,
20999                    };
21000                    {
21001                        return Ok(Some(expr));
21002                    }
21003                }
21004                if !matches!(self.peek(), Token::Null) {
21005                    return Err(self.err(format!(
21006                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21007                    if negated { " NOT" } else { "" },
21008                    self.peek()
21009                )));
21010                }
21011                self.advance();
21012                expr = Expr::IsNull {
21013                    expr: Box::new(expr),
21014                    negated,
21015                };
21016                {
21017                    return Ok(Some(expr));
21018                }
21019            }
21020        }
21021        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21022        if min_prec <= 5 {
21023            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21024            // Look one token ahead so a stray `NOT` not followed by any of
21025            // these flows through to the early return below untouched.
21026            let negated = if matches!(self.peek(), Token::Not) {
21027                let next = self.tokens.get(self.pos + 1);
21028                matches!(next, Some(Token::Between | Token::In | Token::Like))
21029                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21030                    || (self.mysql_dialect
21031                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21032                    || s.eq_ignore_ascii_case("similar"))
21033            } else {
21034                false
21035            };
21036            if negated {
21037                self.advance();
21038            }
21039            if matches!(self.peek(), Token::Between) {
21040                expr = self.parse_between_tail(expr, negated)?;
21041                {
21042                    return Ok(Some(expr));
21043                }
21044            }
21045            if matches!(self.peek(), Token::In) {
21046                if self.suppress_in_tail && !negated {
21047                    // POSITION(sub IN str) — IN belongs to the
21048                    // enclosing function syntax; stop here.
21049                    {
21050                        return Ok(None);
21051                    }
21052                }
21053                expr = self.parse_in_tail(expr, negated)?;
21054                {
21055                    return Ok(Some(expr));
21056                }
21057            }
21058            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21059            // lowers onto the internal __similar_to(expr, pat[, esc]) call
21060            // (the SQL→regex transform runs inside, in the backtracking-
21061            // friendly shape SPG's matcher needs).
21062            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21063                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21064            {
21065                self.advance(); // SIMILAR
21066                self.advance(); // TO
21067                let pattern = self.parse_expr(6)?;
21068                let mut args = alloc::vec![expr, pattern];
21069                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21070                    self.advance();
21071                    args.push(self.parse_expr(6)?);
21072                }
21073                let call = Expr::FunctionCall {
21074                    name: "__similar_to".to_string(),
21075                    args,
21076                };
21077                expr = maybe_not(call, negated);
21078                {
21079                    return Ok(Some(expr));
21080                }
21081            }
21082            if matches!(self.peek(), Token::Like) {
21083                self.advance();
21084                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21085                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21086                    expr = q;
21087                    {
21088                        return Ok(Some(expr));
21089                    }
21090                }
21091                // Pattern at the same precedence as other comparison RHSes —
21092                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21093                let mut pattern = self.parse_expr(6)?;
21094                // `ESCAPE 'c'` — rewrite a literal pattern to the
21095                // default backslash escape at parse time. Custom
21096                // escapes on non-literal patterns would need
21097                // matcher support; error honestly.
21098                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21099                    self.advance();
21100                    let esc = self.parse_expr(6)?;
21101                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21102                }
21103                expr = Expr::Like {
21104                    expr: Box::new(expr),
21105                    pattern: Box::new(pattern),
21106                    negated,
21107                    case_insensitive: false,
21108                };
21109                {
21110                    return Ok(Some(expr));
21111                }
21112            }
21113            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21114            // keyword reaches us as a plain identifier.
21115            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21116                self.advance();
21117                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21118                    expr = q;
21119                    {
21120                        return Ok(Some(expr));
21121                    }
21122                }
21123                let pattern = self.parse_expr(6)?;
21124                expr = Expr::Like {
21125                    expr: Box::new(expr),
21126                    pattern: Box::new(pattern),
21127                    negated,
21128                    case_insensitive: true,
21129                };
21130                {
21131                    return Ok(Some(expr));
21132                }
21133            }
21134            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21135            // operator (RLIKE is the alias). It is a keyword, not `~`, and
21136            // matches case-insensitively under the default collation, so it
21137            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21138            // `~*` operator uses, wrapped in NOT when negated.
21139            if self.mysql_dialect
21140                && matches!(self.peek(), Token::Ident(s)
21141                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21142            {
21143                self.advance();
21144                let pattern = self.parse_expr(6)?;
21145                let call = Expr::FunctionCall {
21146                    name: String::from("regexp_like"),
21147                    args: alloc::vec![
21148                        expr,
21149                        pattern,
21150                        Expr::Literal(Literal::String(String::from("i"))),
21151                    ],
21152                };
21153                return Ok(Some(maybe_not(call, negated)));
21154            }
21155        }
21156        let _ = expr;
21157        Ok(None)
21158    }
21159
21160    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21161        let mut lhs = self.parse_unary()?;
21162        let mut chain_len = 0usize;
21163        loop {
21164            // OPERATOR([schema.]op) reduces to its underlying
21165            // operator token before the normal dispatch.
21166            let explicit = self.peek_explicit_operator();
21167            let dispatch = match &explicit {
21168                Some((_, tok)) => self.binop_here(tok),
21169                None => self.binop_here(self.peek()),
21170            };
21171            let Some((op, prec)) = dispatch else {
21172                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21173                // of the symbol family. `binop_here` answers None for them
21174                // because they lower onto function calls rather than a
21175                // BinOp, and the fallback below reads `self.peek()` — the
21176                // word OPERATOR, not the operator. `pg_dump` writes every
21177                // catalog predicate this way, so its first query failed
21178                // and no dump ran:
21179                //
21180                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21181                //
21182                // Collapsing the wrapper to the operator it names puts the
21183                // token where the fallback already looks.
21184                if let Some((next, op_tok)) = explicit {
21185                    self.tokens.splice(self.pos..next, [op_tok]);
21186                }
21187                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21188                    lhs = e;
21189                    chain_len += 1;
21190                    if chain_len > MAX_BINARY_CHAIN {
21191                        return Err(self.err(alloc::format!(
21192                            "more than {MAX_BINARY_CHAIN} chained binary operators"
21193                        )));
21194                    }
21195                    continue;
21196                }
21197                break;
21198            };
21199            if prec < min_prec {
21200                break;
21201            }
21202            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21203            // iteratively but evaluates and drops recursively;
21204            // depth beyond the budget overflows worker stacks.
21205            chain_len += 1;
21206            if chain_len > MAX_BINARY_CHAIN {
21207                return Err(self.err(alloc::format!(
21208                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21209                )));
21210            }
21211            match explicit {
21212                Some((end_pos, _)) => self.pos = end_pos,
21213                None => {
21214                    self.advance();
21215                }
21216            }
21217            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21218            // ANY is a bare ident; ALL is a reserved Token. Both
21219            // require an immediate `(` to disambiguate from
21220            // identifier columns named `any` / `all`.
21221            let any_kind = match self.peek() {
21222                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21223                    Some(false)
21224                }
21225                Token::Ident(s) | Token::QuotedIdent(s)
21226                    if (s.eq_ignore_ascii_case("any")
21227                        || s.eq_ignore_ascii_case("some")
21228                        || s.eq_ignore_ascii_case("all"))
21229                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21230                {
21231                    Some(!s.eq_ignore_ascii_case("all"))
21232                }
21233                _ => None,
21234            };
21235            if let Some(is_any) = any_kind {
21236                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21237                continue;
21238            }
21239            let rhs = self.parse_expr(prec + 1)?;
21240            lhs = Expr::Binary {
21241                lhs: Box::new(lhs),
21242                op,
21243                rhs: Box::new(rhs),
21244            };
21245        }
21246        Ok(lhs)
21247    }
21248
21249    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21250    /// and the array form.
21251    ///
21252    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21253    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21254    /// this block's `Expr` temporaries and four `format!` sites slots in
21255    /// that frame on every level of `((((1))))`, which never reaches it.
21256    #[inline(never)]
21257    fn parse_any_all_rhs(
21258        &mut self,
21259        lhs: Expr,
21260        op: BinOp,
21261        is_any: bool,
21262    ) -> Result<Expr, ParseError> {
21263        self.advance(); // ident
21264        self.advance(); // (
21265        // `x op ANY (SELECT …)` — the quantified-subquery
21266        // form. `= ANY` is exactly IN; the other operators
21267        // lower onto EXISTS over the subquery as a derived
21268        // table, comparing against its single projection
21269        // aliased __v (x's columns resolve correlated).
21270        // ALL is the negated-EXISTS complement; a NULL
21271        // element makes PG return NULL where this lowering
21272        // returns true — the NOT NULL column case (the
21273        // practical one) is exact.
21274        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21275            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21276            // legal PG too (round-151 sibling). Out-of-line
21277            // (#[inline(never)] helper) — this sits on
21278            // parse_expr's recursive frame and the two-armed
21279            // SELECT temporary blew the nesting-budget stack.
21280            let mut sub = self.parse_any_all_select_body()?;
21281            if !matches!(self.peek(), Token::RParen) {
21282                return Err(self.err(alloc::format!(
21283                    "expected ')' after ANY/ALL subquery, got {:?}",
21284                    self.peek()
21285                )));
21286            }
21287            self.advance();
21288            if sub.items.len() != 1 {
21289                return Err(self.err(alloc::format!(
21290                    "ANY/ALL subquery must return one column, got {}",
21291                    sub.items.len()
21292                )));
21293            }
21294            if is_any && matches!(op, BinOp::Eq) {
21295                return Ok(Expr::InSubquery {
21296                    expr: Box::new(lhs),
21297                    subquery: Box::new(sub),
21298                    negated: false,
21299                });
21300            }
21301            // The engine's subquery resolvers materialise
21302            // the single-column result into an ARRAY the
21303            // existing AnyAll three-valued eval consumes.
21304            return Ok(Expr::AnyAll {
21305                expr: Box::new(lhs),
21306                op,
21307                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21308                is_any,
21309            });
21310        }
21311        let arr = self.parse_expr(0)?;
21312        if !matches!(self.peek(), Token::RParen) {
21313            return Err(self.err(alloc::format!(
21314                "expected ')' after ANY/ALL argument, got {:?}",
21315                self.peek()
21316            )));
21317        }
21318        self.advance();
21319        Ok(Expr::AnyAll {
21320            expr: Box::new(lhs),
21321            op,
21322            array: Box::new(arr),
21323            is_any,
21324        })
21325    }
21326
21327    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21328    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21329    #[inline(never)]
21330    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21331        self.advance();
21332        let e = self.parse_expr(9)?;
21333        Ok(build_center_call(e))
21334    }
21335
21336    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21337    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21338    /// unary minus.
21339    ///
21340    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21341    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21342    /// the Expr-sized local stays out of that frame.
21343    #[inline(never)]
21344    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21345        self.advance();
21346        let e = self.parse_expr(9)?;
21347        Ok(Expr::FunctionCall {
21348            name: alloc::string::String::from(name),
21349            args: alloc::vec![e],
21350        })
21351    }
21352
21353    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21354    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21355    #[inline(never)]
21356    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21357        self.advance();
21358        let e = self.parse_expr(9)?;
21359        Ok(Expr::FunctionCall {
21360            name: alloc::string::String::from(if vertical {
21361                "isvertical"
21362            } else {
21363                "ishorizontal"
21364            }),
21365            args: alloc::vec![e],
21366        })
21367    }
21368
21369    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21370    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21371    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21372    #[inline(never)]
21373    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21374        self.advance();
21375        let e = self.parse_expr(9)?;
21376        Ok(Expr::Cast {
21377            expr: Box::new(e),
21378            target: CastTarget::Named("binary".to_string()),
21379        })
21380    }
21381
21382    /// The prefix operators that share one shape: take the token, parse
21383    /// an operand at `prec`, wrap it.
21384    ///
21385    /// `#[inline(never)]`, and one function instead of five arms, for the
21386    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21387    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21388    /// debug build gives EVERY arm's locals a slot in the frame, whichever
21389    /// arm runs. `((((1))))` reaches none of these arms and was carrying
21390    /// five `Expr`-sized locals per level for them anyway.
21391    #[inline(never)]
21392    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21393        self.advance();
21394        let e = self.parse_expr(prec)?;
21395        Ok(Expr::Unary {
21396            op,
21397            expr: Box::new(e),
21398        })
21399    }
21400
21401    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21402    /// and separate from it because of the literal folding below and the
21403    /// `format!` temporaries that folding needs.
21404    #[inline(never)]
21405    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21406        self.advance();
21407        // v7.39 (round 549) — fold the sign into an integer literal that
21408        // only fits once it is negative.
21409        //
21410        // `9223372036854775808` is one past i64::MAX, so the lexer hands
21411        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21412        // folds the sign first, so `-9223372036854775808` is a bigint
21413        // there — and `-9223372036854775808 - 1` raises "bigint out of
21414        // range" where SPG quietly answered -9223372036854775809, a value
21415        // no bigint can hold. The arithmetic itself was already checked;
21416        // only the literal's type was wrong.
21417        if let Token::Numeric(lit) = self.peek()
21418            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21419        {
21420            self.advance();
21421            return Ok(Expr::Literal(Literal::Integer(folded)));
21422        }
21423        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21424        // `<->` slotted into 5 and arithmetic shifted up).
21425        let e = self.parse_expr(9)?;
21426        Ok(Expr::Unary {
21427            op: UnOp::Neg,
21428            expr: Box::new(e),
21429        })
21430    }
21431
21432    /// tsquery `!!` prefix negation, lowered to the catalog function.
21433    /// Binds like unary minus. Out-of-line for the frame reason on
21434    /// `parse_unary_op`.
21435    #[inline(never)]
21436    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21437        self.advance();
21438        let e = self.parse_expr(9)?;
21439        Ok(Expr::FunctionCall {
21440            name: String::from("tsquery_not"),
21441            args: alloc::vec![e],
21442        })
21443    }
21444
21445    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21446        match self.peek() {
21447            // NOT binds tighter than AND / XOR / OR but looser than
21448            // comparisons — its operand takes everything ≥ the comparison
21449            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21450            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21451            // was rung 3, behaviour-identical when 3 was unused; AND now
21452            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21453            Token::Not => self.parse_unary_op(UnOp::Not, 4),
21454            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21455            // The body is out-of-line: `parse_unary` is one of the three
21456            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21457            // inline arm here overflowed the native stack in
21458            // `nesting_budget_errors_cleanly` — the guard test caught it,
21459            // exactly as the eval-side cliff did in rounds 346 and 351.
21460            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21461                self.parse_binary_prefix()
21462            }
21463            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21464            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21465            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21466            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21467            Token::Minus => self.parse_prefix_minus(),
21468            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21469            // worked only because the lexer reads it as one signed literal;
21470            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21471            // PG18 and MariaDB take all of them. Binds like unary minus.
21472            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21473            // Bitwise NOT binds like unary minus.
21474            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21475            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21476            // "center of" operator; desugars to center(x). The whole arm
21477            // is out-of-line: parse_unary sits on the per-nesting-level
21478            // frame chain that MAX_NEST_DEPTH is tuned against, so no
21479            // Expr-sized local may live in this frame.
21480            Token::TsMatch => self.parse_prefix_center(),
21481            // v7.39 (round 508) — the prefix operators that are named
21482            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21483            // is length. Out-of-line for the same nesting-frame reason as
21484            // parse_prefix_center — parse_unary sits on the recursive cycle
21485            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21486            // live in this frame.
21487            Token::At => self.parse_prefix_call("abs"),
21488            Token::Hash => self.parse_prefix_call("npoints"),
21489            Token::AtMinusAt => self.parse_prefix_call("length"),
21490            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21491            // "is horizontal" (lseg / line); desugars to the existing
21492            // isvertical()/ishorizontal() functions. Out-of-line for the
21493            // same nesting-frame reason as parse_prefix_center.
21494            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21495            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21496            Token::DoubleBang => self.parse_prefix_tsquery_not(),
21497            _ => self.parse_atom(),
21498        }
21499    }
21500
21501    /// Parse a parenthesised scalar subquery body after the caller has consumed
21502    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21503    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21504    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21505    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21506    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21507    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21508    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21509    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21510    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21511    /// tips the deep-nesting test into a stack overflow).
21512    #[inline(never)]
21513    fn array_subquery_ahead(&self) -> bool {
21514        if !matches!(self.peek(), Token::LParen) {
21515            return false;
21516        }
21517        matches!(
21518            self.tokens.get(self.pos + 1),
21519            Some(Token::Select | Token::Values)
21520        ) || matches!(
21521            self.tokens.get(self.pos + 1),
21522            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21523        )
21524    }
21525
21526    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21527    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21528    /// locals stay off parse_atom's recursive frame (round 105).
21529    #[inline(never)]
21530    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21531        self.advance(); // consume `[`
21532        let mut items: Vec<Expr> = Vec::new();
21533        if !matches!(self.peek(), Token::RBracket) {
21534            loop {
21535                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21536                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21537                if matches!(self.peek(), Token::LBracket) {
21538                    items.push(self.parse_array_bracket_body()?);
21539                } else {
21540                    items.push(self.parse_expr(0)?);
21541                }
21542                match self.peek() {
21543                    Token::Comma => {
21544                        self.advance();
21545                    }
21546                    Token::RBracket => break,
21547                    other => {
21548                        return Err(self.err(alloc::format!(
21549                            "expected ',' or ']' in ARRAY literal, got {other:?}"
21550                        )));
21551                    }
21552                }
21553            }
21554        }
21555        self.advance(); // consume `]`
21556        Ok(Expr::Array(items))
21557    }
21558
21559    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21560    /// is already consumed; the current token is `(`. Desugars to a scalar
21561    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21562    /// the subquery's single-column rows in order — reusing the existing
21563    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21564    /// keeps the large `Statement` local off parse_atom's recursive frame.
21565    #[inline(never)]
21566    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21567        self.advance(); // consume `(`
21568        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21569            if w.eq_ignore_ascii_case("with"));
21570        let sub = if is_with {
21571            self.advance(); // WITH
21572            self.parse_with_cte_then_select()?
21573        } else {
21574            self.parse_select_stmt()?
21575        };
21576        if !matches!(self.peek(), Token::RParen) {
21577            return Err(self.err(alloc::format!(
21578                "expected ')' to close ARRAY(subquery), got {:?}",
21579                self.peek()
21580            )));
21581        }
21582        self.advance(); // consume `)`
21583        // Reuse the parser to build the array_agg wrapper from the subquery's
21584        // canonical text — avoids hand-constructing the derived-table AST.
21585        let wrapper = alloc::format!(
21586            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21587        );
21588        let stmt = parse_statement(&wrapper)
21589            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21590        let Statement::Select(sel) = stmt else {
21591            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21592        };
21593        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21594    }
21595
21596    #[inline(never)]
21597    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21598        let inner = if is_with {
21599            self.advance(); // WITH
21600            self.parse_with_cte_then_select()?
21601        } else {
21602            self.parse_select_stmt()?
21603        };
21604        match self.advance() {
21605            Token::RParen => {
21606                let Statement::Select(s) = inner else {
21607                    return Err(ParseError {
21608                        message: "scalar subquery body must be a SELECT".into(),
21609                        token_pos: self.consumed_pos(),
21610                    });
21611                };
21612                Ok(Expr::ScalarSubquery(Box::new(s)))
21613            }
21614            other => Err(ParseError {
21615                message: format!("expected ')' after scalar subquery, got {other:?}"),
21616                token_pos: self.consumed_pos(),
21617            }),
21618        }
21619    }
21620
21621    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
21622    /// literals. The lexer splits them into an ident + string; recombine
21623    /// here. Out-of-line and returning `Option` so `parse_atom` — the
21624    /// recursive frame the 768 KiB stack budget is tuned against — pays no
21625    /// frame for the `body` / `bits` strings and their char loops (the
21626    /// round-367 frame cliff, M20).
21627    #[inline(never)]
21628    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
21629        let is_hex = match self.peek() {
21630            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
21631            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
21632            _ => return None,
21633        };
21634        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
21635            return None;
21636        }
21637        self.advance();
21638        let Token::String(body) = self.advance() else {
21639            unreachable!("guarded above");
21640        };
21641        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
21642        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
21643        // (hex pairs, even count required — MariaDB errors on an odd
21644        // count); `b'1010'` packs its bits big-endian, left-padded to a
21645        // byte. Lower both onto the bytea cast.
21646        if self.mysql_dialect {
21647            if is_hex {
21648                if body.len() % 2 == 1 {
21649                    return Some(Err(self.err(alloc::format!(
21650                        "invalid hex string literal X'{body}': odd digit count"
21651                    ))));
21652                }
21653                for c in body.chars() {
21654                    if !c.is_ascii_hexdigit() {
21655                        return Some(Err(
21656                            self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
21657                        ));
21658                    }
21659                }
21660                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
21661            }
21662            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21663                return Some(Err(
21664                    self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
21665                ));
21666            }
21667            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
21668        }
21669        let bits = if is_hex {
21670            let mut out = String::with_capacity(body.len() * 4);
21671            for c in body.chars() {
21672                let Some(d) = c.to_digit(16) else {
21673                    return Some(Err(self.err(alloc::format!(
21674                        "invalid hexadecimal digit {c:?} in X'…' bit string"
21675                    ))));
21676                };
21677                out.push_str(&alloc::format!("{d:04b}"));
21678            }
21679            out
21680        } else {
21681            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21682                return Some(Err(self.err(alloc::format!(
21683                    "invalid binary digit {bad:?} in B'…' bit string"
21684                ))));
21685            }
21686            body
21687        };
21688        // Route through the postfix-cast loop so a chained cast like
21689        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
21690        // of erroring at the `::`.
21691        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
21692        // literal keeps its exact length, while an explicit `::bit` cast is
21693        // bit(1) with pad/truncate semantics (PG).
21694        Some(self.finish_postfix_casts(Expr::Cast {
21695            expr: Box::new(Expr::Literal(Literal::String(bits))),
21696            target: CastTarget::Named("__bit_literal".to_string()),
21697        }))
21698    }
21699
21700    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
21701        if let Some(res) = self.try_parse_bit_string_literal() {
21702            return res;
21703        }
21704        let tok_pos = self.pos;
21705        match self.advance() {
21706            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
21707            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
21708            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
21709            // carrying the source mantissa + scale so no precision is lost. A
21710            // literal too wide for i128 falls back to double precision.
21711            // Out-of-line (#[inline(never)]) — this arm sits on the
21712            // parse_expr recursion chain; its expansion locals must not
21713            // widen the recursive frame (debug frame-cliff discipline).
21714            Token::Numeric(s) => match numeric_token_to_literal(s) {
21715                Ok(lit) => Ok(Expr::Literal(lit)),
21716                Err(msg) => Err(self.err(msg)),
21717            },
21718            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
21719            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
21720            // (the lexer only emits this token in the MySQL dialect). Lower
21721            // onto the existing bytea cast; out-of-line to keep this arm off
21722            // the parse recursion frame.
21723            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
21724            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
21725            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
21726            Token::Null => Ok(Expr::Literal(Literal::Null)),
21727            // v6.1.1 — `$N` placeholder. The actual Value lookup
21728            // happens in the engine eval path against the prepared-
21729            // statement bind buffer.
21730            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
21731            Token::LParen => {
21732                // v4.10: `(SELECT ...)` in expression position is a
21733                // scalar subquery; otherwise it's a parenthesised
21734                // expression. Peek for SELECT keyword to dispatch.
21735                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
21736                // lexes as Ident("with") (not a reserved token). The subquery body
21737                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
21738                // so its large `Statement` local stays out of parse_atom's stack
21739                // frame — parse_atom is on the recursive `((…))` cycle and the
21740                // nesting budget is tuned to its frame size).
21741                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21742                    if s.eq_ignore_ascii_case("with"));
21743                if matches!(self.peek(), Token::Select) || is_with {
21744                    self.parse_paren_scalar_subquery(is_with)
21745                } else {
21746                    let e = self.parse_expr(0)?;
21747                    // `(a, b, …)` — a row constructor. Valid only
21748                    // in front of a comparison operator or [NOT]
21749                    // IN; both expand at parse time (lexicographic
21750                    // comparison / OR'd row equalities).
21751                    if matches!(self.peek(), Token::Comma) {
21752                        let mut row = alloc::vec![e];
21753                        while matches!(self.peek(), Token::Comma) {
21754                            self.advance();
21755                            row.push(self.parse_expr(0)?);
21756                        }
21757                        if !matches!(self.peek(), Token::RParen) {
21758                            return Err(self.err(alloc::format!(
21759                                "expected ')' after row constructor, got {:?}",
21760                                self.peek()
21761                            )));
21762                        }
21763                        self.advance();
21764                        // A bare `(a, b, …)` row constructor can carry postfix
21765                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
21766                        // early return here skips parse_atom's tail postfix
21767                        // pass, so fold casts in explicitly. For the
21768                        // comparison / predicate forms nothing postfix follows,
21769                        // so this is a no-op.
21770                        return self
21771                            .parse_row_comparison_tail(row)
21772                            .and_then(|e| self.finish_postfix_casts(e));
21773                    }
21774                    match self.advance() {
21775                        Token::RParen => Ok(e),
21776                        other => Err(ParseError {
21777                            message: format!("expected ')', got {other:?}"),
21778                            token_pos: self.consumed_pos(),
21779                        }),
21780                    }
21781                }
21782            }
21783            Token::LBracket => self.parse_vector_literal_body(),
21784            Token::Extract => self.parse_extract_atom(),
21785            Token::Interval => self.parse_interval_atom(),
21786            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
21787            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
21788            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
21789            // expression position calling the PG `left(string, n)` /
21790            // `right(string, n)` function; rebuild the AST as a regular
21791            // function call so the engine's apply_function dispatch picks
21792            // it up. Delegated to a #[inline(never)] helper so its locals
21793            // don't bloat this recursive `parse_atom` frame (the nesting
21794            // budget in `enter_nested` is tuned to parse_atom's size).
21795            Token::Left if matches!(self.peek(), Token::LParen) => {
21796                self.parse_lr_string_function_call("left")
21797            }
21798            Token::Right if matches!(self.peek(), Token::LParen) => {
21799                self.parse_lr_string_function_call("right")
21800            }
21801            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
21802            // token; we match on the bare ident. NOT is a token
21803            // (consumed in the comparison rung), but `EXISTS (...)`
21804            // at the top of an expression starts here.
21805            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
21806                self.parse_exists_atom(false)
21807            }
21808            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
21809            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
21810            // CASE is a bare ident; we dispatch on lowercase match.
21811            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
21812                self.parse_case_atom()
21813            }
21814            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
21815            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
21816            // '…'`. Lower onto the ::cast node so the existing
21817            // runtime text→date/timestamp paths do the parsing. The
21818            // string must follow immediately, else the ident stays a
21819            // plain column reference.
21820            Token::Ident(s)
21821                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
21822                    && matches!(self.peek(), Token::String(_)) =>
21823            {
21824                let target =
21825                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
21826                let Token::String(lit) = self.advance() else {
21827                    unreachable!("peek guaranteed a string token");
21828                };
21829                Ok(Expr::Cast {
21830                    expr: Box::new(Expr::Literal(Literal::String(lit))),
21831                    target,
21832                })
21833            }
21834            // v7.39 (round 221) — the SQL-standard long spellings:
21835            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
21836            // TIME ZONE '…'`. Consume the modifier and lower to the same
21837            // typed-literal cast (`timetz` / `timestamptz` for WITH).
21838            Token::Ident(s)
21839                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
21840                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
21841                        || w.eq_ignore_ascii_case("without"))
21842                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
21843                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
21844                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
21845            {
21846                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
21847                self.advance(); // WITH / WITHOUT
21848                self.advance(); // TIME
21849                self.advance(); // ZONE
21850                let Token::String(lit) = self.advance() else {
21851                    unreachable!("guard checked a string token");
21852                };
21853                let base = s.to_ascii_lowercase();
21854                let target = match (base.as_str(), with_tz) {
21855                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
21856                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
21857                    (_, true) => CastTarget::Timestamptz,
21858                    (_, false) => CastTarget::Timestamp,
21859                };
21860                Ok(Expr::Cast {
21861                    expr: Box::new(Expr::Literal(Literal::String(lit))),
21862                    target,
21863                })
21864            }
21865            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
21866            // gathers the subquery's single-column rows (in its row order)
21867            // into an array. Desugared to `array_agg` over the subquery as a
21868            // derived table; out-of-line to keep parse_atom's frame small (it
21869            // sits on the recursive nesting-budget cycle).
21870            Token::Ident(s) | Token::QuotedIdent(s)
21871                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
21872            {
21873                self.parse_array_subquery()
21874            }
21875            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
21876            // is not a reserved token; we match by case-insensitive
21877            // ident. The opening `[` must follow immediately. v7.39 (read01
21878            // round 105) — the body moved out-of-line so its `Vec`/loop locals
21879            // leave parse_atom's frame (which sits on the nesting-budget cycle).
21880            Token::Ident(s) | Token::QuotedIdent(s)
21881                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
21882            {
21883                self.parse_array_literal_body()
21884            }
21885            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
21886            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
21887            // We special-case before the generic ident dispatch so
21888            // the AGAINST clause never reaches the function-call
21889            // loop (which would mis-read `(cols) AGAINST` as a
21890            // call with no trailing modifier). The shape is
21891            // rewritten to a Boolean OR over per-column
21892            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
21893            // term)` so the existing FTS evaluator handles
21894            // semantics — the fulltext-GIN built at CREATE TABLE
21895            // time is currently a "real index that survives dump
21896            // round-trip"; the planner hook that actually uses
21897            // it for posting-list intersection lands in a later
21898            // sub-phase (Phase 2.2b) without touching this surface.
21899            Token::Ident(s) | Token::QuotedIdent(s)
21900                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
21901            {
21902                self.parse_match_against_atom()
21903            }
21904            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
21905            // v7.37.43-T4 — PG-unreserved keywords are legal column /
21906            // alias names in expression context too. `release` appears
21907            // in sentori `0003_partition_events.sql` as both a column
21908            // reference (SELECT … release …) and an INSERT column list
21909            // entry. Mirrors `expect_ident_like`'s expansion of the
21910            // identifier set.
21911            other if unreserved_keyword_text(&other).is_some() => {
21912                let s = unreserved_keyword_text(&other).unwrap();
21913                self.finish_ident_atom(s)
21914            }
21915            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
21916            // only inside `SET` before, so `SELECT @@autocommit` — which
21917            // every MySQL connector asks at handshake — was a parse error.
21918            // MariaDB accepts the bare, `@@session.` and `@@global.`
21919            // spellings alike and answers from the session's own value.
21920            Token::SessionVar(v) => {
21921                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
21922                // has nothing to do with a `@@` engine setting: its own
21923                // per-session namespace, and an unset one reads NULL instead
21924                // of raising. Stripping every `@` (as this did) made `@x` and
21925                // `@@x` the same node, so `SELECT @x` answered "Unknown
21926                // system variable".
21927                Ok(variable_ref_atom(&v))
21928            }
21929            other => Err(ParseError {
21930                message: format!("unexpected token {other:?} in expression"),
21931                token_pos: tok_pos,
21932            }),
21933        }
21934        // After parsing the atom, fold any postfix `::vector` casts.
21935        .and_then(|atom| self.finish_postfix_casts(atom))
21936    }
21937
21938    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
21939    /// Both bind tighter than any binary op.
21940    /// Shared cast-target parser for postfix `::TYPE` and the
21941    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
21942    /// If the next tokens are `( N )`, consume them and return the canonical
21943    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
21944    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
21945    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
21946        if !matches!(self.peek(), Token::LParen) {
21947            return None;
21948        }
21949        self.advance(); // (
21950        let n = match self.advance() {
21951            Token::Integer(n) => n,
21952            _ => return Some(base.to_string()), // malformed → drop precision
21953        };
21954        if matches!(self.peek(), Token::RParen) {
21955            self.advance();
21956        }
21957        Some(alloc::format!("{base}({n})"))
21958    }
21959
21960    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
21961        let target = match self.advance() {
21962            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
21963                "int" | "integer" | "int4" => {
21964                    if matches!(self.peek(), Token::LBracket)
21965                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
21966                    {
21967                        self.advance();
21968                        self.advance();
21969                        CastTarget::IntArray
21970                    } else {
21971                        CastTarget::Int
21972                    }
21973                }
21974                "bigint" | "int8" => {
21975                    if matches!(self.peek(), Token::LBracket)
21976                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
21977                    {
21978                        self.advance();
21979                        self.advance();
21980                        CastTarget::BigIntArray
21981                    } else {
21982                        CastTarget::BigInt
21983                    }
21984                }
21985                "float" | "double" => CastTarget::Float,
21986                "text" => {
21987                    // v7.10.11 — `::TEXT[]` widens to TextArray.
21988                    if matches!(self.peek(), Token::LBracket)
21989                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
21990                    {
21991                        self.advance();
21992                        self.advance();
21993                        CastTarget::TextArray
21994                    } else {
21995                        CastTarget::Text
21996                    }
21997                }
21998                "bool" | "boolean" => CastTarget::Bool,
21999                "vector" => CastTarget::Vector,
22000                "date" => CastTarget::Date,
22001                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22002                // seconds precision through the Named path (the engine rounds
22003                // the sub-second field); bare `::timestamp` keeps the fast arm.
22004                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22005                    Some(named) => CastTarget::Named(named),
22006                    None => CastTarget::Timestamp,
22007                },
22008                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22009                    Some(named) => CastTarget::Named(named),
22010                    None => CastTarget::Timestamptz,
22011                },
22012                "interval" => CastTarget::Interval,
22013                "json" => CastTarget::Json,
22014                "jsonb" => CastTarget::Jsonb,
22015                // v7.39 (round 694) — these have dedicated CastTarget
22016                // variants, so they never reached the postfix `[]` handling
22017                // further down and `::regtype[]` was a SYNTAX error at the
22018                // `]`. PG has an array type for every scalar; take the
22019                // suffix here and hand the canonical `<ty>_array` name to
22020                // the engine, the same shape every other array cast uses.
22021                "regtype" if self.peek_postfix_array_brackets() => {
22022                    self.advance();
22023                    self.advance();
22024                    CastTarget::Named(alloc::string::String::from("regtype_array"))
22025                }
22026                "regclass" if self.peek_postfix_array_brackets() => {
22027                    self.advance();
22028                    self.advance();
22029                    CastTarget::Named(alloc::string::String::from("regclass_array"))
22030                }
22031                "regtype" => CastTarget::RegType,
22032                "regclass" => CastTarget::RegClass,
22033                // v7.12.0 — `::tsvector` / `::tsquery`.
22034                // Engine decodes the LHS text via the PG
22035                // external form parser.
22036                // v7.39 (round 352, M8) — MySQL's own cast targets.
22037                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22038                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22039                // such type, so they are taken only in that dialect and
22040                // fall through to the "type does not exist" arm otherwise.
22041                "signed" | "unsigned" if self.mysql_dialect => {
22042                    if matches!(self.peek(), Token::Ident(k)
22043                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22044                    {
22045                        self.advance();
22046                    }
22047                    CastTarget::Named(s.to_ascii_lowercase())
22048                }
22049                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22050                // in MySQL: MariaDB answers '123' where the SQL-standard
22051                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22052                // Truncating a number to its first digit is a wrong answer
22053                // with no error, so the MySQL session gets MySQL's reading.
22054                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22055                    CastTarget::Text
22056                }
22057                "tsvector" => CastTarget::TsVector,
22058                "tsquery" => CastTarget::TsQuery,
22059                // v7.17.0 — `::uuid`. Engine decodes the LHS
22060                // text via `spg_storage::parse_uuid_str`.
22061                "uuid" => CastTarget::Uuid,
22062                // v7.18 — `::bytea`. Engine decodes the LHS
22063                // text via the PG hex form (`'\xdeadbeef'`)
22064                // or escape form (`'\\x05\\x00'`). Closes
22065                // mailrs D-pre #3 reverse-acceptance gap.
22066                "bytea" => CastTarget::Bytea,
22067                // v7.37.5 ship triage — generic typed-cast escape.
22068                // Anything the long-tail PG type ident table knows
22069                // about(network/bit/geometry/multirange/etc.)flows
22070                // through `CastTarget::Named(canonical)`; the engine
22071                // resolves via `column_type_to_data_type` and dispatches
22072                // through the typed `coerce_value` path. Truly
22073                // unrecognised idents still hit the error arm below
22074                // because the engine rejects them.
22075                other => {
22076                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22077                    // `::varchar(255)`, etc. Capture into the canonical
22078                    // `name(p,s)` form so `type_name_to_data_type` can
22079                    // reconstruct the `DataType::Numeric { precision,
22080                    // scale }` (and similar param-carrying types).
22081                    let mut name = other.to_string();
22082                    // v7.39 (round 281) — `::bit varying(3)` is two
22083                    // words; fold the tail in so the typmod reaches the
22084                    // type resolver instead of tripping the parser.
22085                    if name.eq_ignore_ascii_case("bit")
22086                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22087                    {
22088                        self.advance();
22089                        name = alloc::string::String::from("varbit");
22090                    }
22091                    // v7.39 (round 613) — `::character varying` is the same
22092                    // two-word shape and had no fold, so the `varying` was
22093                    // left behind and the cast became a bare `character`,
22094                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22095                    // `a` where PG answers `ab`. Silently, and for a spelling
22096                    // pg_dump writes.
22097                    if name.eq_ignore_ascii_case("character")
22098                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22099                    {
22100                        self.advance();
22101                        name = alloc::string::String::from("varchar");
22102                    }
22103                    if matches!(self.peek(), Token::LParen) {
22104                        let mut buf = alloc::string::String::from("(");
22105                        let mut depth = 0usize;
22106                        loop {
22107                            match self.advance() {
22108                                Token::LParen => {
22109                                    depth += 1;
22110                                    if depth > 1 {
22111                                        buf.push('(');
22112                                    }
22113                                }
22114                                Token::RParen => {
22115                                    depth -= 1;
22116                                    if depth == 0 {
22117                                        buf.push(')');
22118                                        break;
22119                                    }
22120                                    buf.push(')');
22121                                }
22122                                Token::Comma => buf.push(','),
22123                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22124                                // v7.39 (round 273) — a minus used to fall
22125                                // into the catch-all below and vanish, so
22126                                // `::numeric(10,-2)` reached the engine as
22127                                // the text `numeric(10,2)` and silently
22128                                // rounded to two DECIMALS instead of to
22129                                // hundreds. A dropped token is not a
22130                                // no-op when it carries a sign.
22131                                Token::Minus => buf.push('-'),
22132                                Token::Eof => break,
22133                                _ => {}
22134                            }
22135                        }
22136                        name.push_str(&buf);
22137                    }
22138                    // Optional postfix `[]` widens to the array form —
22139                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22140                    // The engine's `type_name_to_data_type` recognises
22141                    // the canonical `<ty>_array` form.
22142                    if matches!(self.peek(), Token::LBracket)
22143                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22144                    {
22145                        self.advance();
22146                        self.advance();
22147                        name.push_str("_array");
22148                    }
22149                    CastTarget::Named(name)
22150                }
22151            },
22152            Token::Interval => CastTarget::Interval,
22153            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22154            // "char" (oid 18, SPG Char1 — distinct from bare `char`
22155            // = char(1)); other quoted names resolve like idents.
22156            Token::QuotedIdent(q) => {
22157                if q.eq_ignore_ascii_case("char") {
22158                    CastTarget::Named("char1".into())
22159                } else {
22160                    CastTarget::Named(q.to_ascii_lowercase())
22161                }
22162            }
22163            other => {
22164                return Err(ParseError {
22165                    message: format!("expected type ident after `::`, got {other:?}"),
22166                    token_pos: self.consumed_pos(),
22167                });
22168            }
22169        };
22170        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22171        // target to its array sibling. Closed-enum arms (Bool /
22172        // SmallInt / Numeric / Float / Date / …) didn't carry the
22173        // explicit widening that Text / Int / BigInt did, so
22174        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22175        // error. The widening here mirrors the per-arm Text /
22176        // Int / BigInt logic above + folds the new ζ-A first-class
22177        // types through `CastTarget::Named("<ty>_array")`.
22178        if matches!(self.peek(), Token::LBracket)
22179            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22180        {
22181            let widened = match &target {
22182                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22183                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22184                // v7.39 (round 326, V43) — the two temporal types stay
22185                // distinct. Both used to widen to `timestamptz_array`, so
22186                // `::timestamp[]` named the wrong target in its own error
22187                // message and lost the zone-less identity on the way.
22188                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22189                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22190                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22191                CastTarget::Json | CastTarget::Jsonb => {
22192                    Some(CastTarget::Named("jsonb_array".to_string()))
22193                }
22194                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22195                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22196                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22197                CastTarget::Named(name) => {
22198                    let mut a = name.clone();
22199                    a.push_str("_array");
22200                    Some(CastTarget::Named(a))
22201                }
22202                // Int / BigInt / Text / Vector / TsVector / TsQuery /
22203                // RegType / RegClass / TextArray / IntArray /
22204                // BigIntArray already finalised — leave as is.
22205                _ => None,
22206            };
22207            if let Some(w) = widened {
22208                self.advance();
22209                self.advance();
22210                return Ok(w);
22211            }
22212        }
22213        Ok(target)
22214    }
22215
22216    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22217        loop {
22218            // v7.38 (read01, T9) — composite field access `(expr).field`.
22219            // A bare `a.b` is consumed as a qualified column inside the ident
22220            // atom, so a Dot only survives to this postfix position when the
22221            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22222            // `.*` whole-row expansion is not handled here (projection-level).
22223            if matches!(self.peek(), Token::Dot)
22224                && matches!(
22225                    self.tokens.get(self.pos + 1),
22226                    Some(Token::Ident(_) | Token::QuotedIdent(_))
22227                )
22228            {
22229                self.advance(); // .
22230                let field = match self.advance() {
22231                    Token::Ident(s) | Token::QuotedIdent(s) => s,
22232                    other => {
22233                        return Err(
22234                            self.err(format!("expected a field name after '.', got {other:?}"))
22235                        );
22236                    }
22237                };
22238                expr = Expr::FieldAccess {
22239                    base: Box::new(expr),
22240                    field,
22241                };
22242                continue;
22243            }
22244            if matches!(self.peek(), Token::DoubleColon) {
22245                self.advance();
22246                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22247                // target set to include INTERVAL (reserved Token),
22248                // TIMESTAMPTZ, and PG catalog regtype / regclass.
22249                // mailrs follow-up H3a + H3b.
22250                let target = self.parse_cast_target()?;
22251                expr = Expr::Cast {
22252                    expr: Box::new(expr),
22253                    target,
22254                };
22255                continue;
22256            }
22257            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22258            // returns NULL for out-of-range. Multiple subscripts
22259            // chain: `a[i][j]` parses left-to-right.
22260            if matches!(self.peek(), Token::LBracket) {
22261                self.advance();
22262                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22263                // bare index stays a subscript.
22264                let lo = if matches!(self.peek(), Token::Colon) {
22265                    None
22266                } else {
22267                    Some(self.parse_expr(0)?)
22268                };
22269                if matches!(self.peek(), Token::Colon) {
22270                    self.advance();
22271                    let hi = if matches!(self.peek(), Token::RBracket) {
22272                        None
22273                    } else {
22274                        Some(Box::new(self.parse_expr(0)?))
22275                    };
22276                    if !matches!(self.peek(), Token::RBracket) {
22277                        return Err(self.err(alloc::format!(
22278                            "expected ']' after array slice, got {:?}",
22279                            self.peek()
22280                        )));
22281                    }
22282                    self.advance();
22283                    expr = Expr::ArraySlice {
22284                        target: Box::new(expr),
22285                        lo: lo.map(Box::new),
22286                        hi,
22287                    };
22288                    continue;
22289                }
22290                let index = lo.expect("non-colon branch parsed an index");
22291                if !matches!(self.peek(), Token::RBracket) {
22292                    return Err(self.err(alloc::format!(
22293                        "expected ']' after array index, got {:?}",
22294                        self.peek()
22295                    )));
22296                }
22297                self.advance();
22298                expr = Expr::ArraySubscript {
22299                    target: Box::new(expr),
22300                    index: Box::new(index),
22301                };
22302                continue;
22303            }
22304            // `expr AT TIME ZONE zone` — lowers to PG's own function
22305            // form timezone(zone, expr); the scalar implements the
22306            // offset shift (named zones error there — no tzdata).
22307            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22308                && matches!(self.tokens.get(self.pos + 1),
22309                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22310                && matches!(self.tokens.get(self.pos + 2),
22311                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22312            {
22313                self.advance(); // AT
22314                self.advance(); // TIME
22315                self.advance(); // ZONE
22316                // Zone at comparison precedence so AND/OR stay out.
22317                let zone = self.parse_expr(6)?;
22318                expr = Expr::FunctionCall {
22319                    name: "timezone".to_string(),
22320                    args: alloc::vec![zone, expr],
22321                };
22322                continue;
22323            }
22324            // `expr COLLATE "name"` — SPG's single text ordering IS
22325            // byte order, i.e. the C collation. The byte-order
22326            // spellings absorb as no-ops; a locale collation would
22327            // silently sort differently from PG, so it errors
22328            // honestly instead.
22329            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22330                self.advance();
22331                let mut cname = match self.advance() {
22332                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22333                    other => {
22334                        return Err(self.err(alloc::format!(
22335                            "expected collation name after COLLATE, got {other:?}"
22336                        )));
22337                    }
22338                };
22339                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22340                // is how `pg_dump` writes the default one:
22341                // `… COLLATE pg_catalog.default`. Reading a single token
22342                // left the SCHEMA as the name, so the clause was refused
22343                // as an unsupported locale collation and no dump ran.
22344                if matches!(self.peek(), Token::Dot) {
22345                    self.advance();
22346                    cname = match self.advance() {
22347                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22348                        // `default` lexes as a KEYWORD, and it is the name
22349                        // pg_dump writes — the same trap round 535 hit with
22350                        // TABLE / INDEX / FULL.
22351                        Token::Default => alloc::string::String::from("default"),
22352                        other => {
22353                            return Err(self.err(alloc::format!(
22354                                "expected collation name after COLLATE, got {other:?}"
22355                            )));
22356                        }
22357                    };
22358                }
22359                let lc = cname.to_ascii_lowercase();
22360                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22361                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22362                // family / `binary`) forces byte-wise, which is exactly
22363                // what `BINARY expr` does — lower onto that so every fold
22364                // site (comparison, LIKE, ORDER BY) suppresses via
22365                // `is_binary_coerced`. A `_ci` family override folds, and
22366                // under the MySQL dialect the default already folds, so it
22367                // absorbs as a no-op; likewise the C / byte-order spellings.
22368                if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22369                    expr = Expr::Cast {
22370                        expr: alloc::boxed::Box::new(expr),
22371                        target: CastTarget::Named("binary".to_string()),
22372                    };
22373                    continue;
22374                }
22375                let mysql_ci = self.mysql_dialect
22376                    && (lc.ends_with("_ci")
22377                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22378                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22379                // goes to the lowering channel, the byte-order spellings
22380                // included. Round 691 recorded only the names the old
22381                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22382                // absorbed as a no-op — and once a column could declare a
22383                // collation, absorbing the clause meant the COLUMN's
22384                // collation won where the query had asked for bytes.
22385                if self.in_order_by_key && !mysql_ci {
22386                    self.order_key_collation = Some(cname);
22387                    continue;
22388                }
22389                if !matches!(
22390                    lc.as_str(),
22391                    "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22392                ) && !mysql_ci
22393                {
22394                    return Err(self.err(alloc::format!(
22395                        "COLLATE {cname:?}: SPG orders text by bytes (the C \
22396                         collation); locale collations are not supported yet — \
22397                         use COLLATE \"C\" or drop the clause"
22398                    )));
22399                }
22400                continue;
22401            }
22402            return Ok(expr);
22403        }
22404    }
22405
22406    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22407    /// the first token that is not one. Schema qualifiers collapse to the
22408    /// last part, which is what every other name path here does (SPG is
22409    /// single-schema).
22410    fn take_comma_separated_names(&mut self) -> Vec<String> {
22411        let mut out = Vec::new();
22412        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22413            self.advance();
22414            let mut last = n;
22415            while matches!(self.peek(), Token::Dot) {
22416                self.advance();
22417                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22418                    last = t;
22419                }
22420            }
22421            out.push(last);
22422            if matches!(self.peek(), Token::Comma) {
22423                self.advance();
22424            } else {
22425                break;
22426            }
22427        }
22428        out
22429    }
22430
22431    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22432    ///
22433    /// The general cast-target path tests this inline; the types with their
22434    /// own `CastTarget` variant need it as a guard on their match arm,
22435    /// which is what this exists for.
22436    fn peek_postfix_array_brackets(&self) -> bool {
22437        matches!(self.peek(), Token::LBracket)
22438            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22439    }
22440
22441    /// Parse the operator tail after a `(a, b, …)` row constructor
22442    /// and expand at parse time. `=` is the conjunction of element
22443    /// equalities; `<>` its negation; the order operators expand
22444    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22445    /// equalities. Anything else (a bare row value, a subquery
22446    /// RHS) errors honestly — SPG has no composite runtime value.
22447    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22448        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22449            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22450                lhs: Box::new(l.clone()),
22451                op: BinOp::Eq,
22452                rhs: Box::new(r.clone()),
22453            });
22454            let first = it.next().expect("row has at least two elements");
22455            it.fold(first, |acc, e| Expr::Binary {
22456                lhs: Box::new(acc),
22457                op: BinOp::And,
22458                rhs: Box::new(e),
22459            })
22460        }
22461        // Lexicographic (a,b) OP (c,d):
22462        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
22463        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22464            if lhs.len() == 1 {
22465                return Expr::Binary {
22466                    lhs: Box::new(lhs[0].clone()),
22467                    op: last,
22468                    rhs: Box::new(rhs[0].clone()),
22469                };
22470            }
22471            let head_strict = Expr::Binary {
22472                lhs: Box::new(lhs[0].clone()),
22473                op: strict,
22474                rhs: Box::new(rhs[0].clone()),
22475            };
22476            let head_eq = Expr::Binary {
22477                lhs: Box::new(lhs[0].clone()),
22478                op: BinOp::Eq,
22479                rhs: Box::new(rhs[0].clone()),
22480            };
22481            Expr::Binary {
22482                lhs: Box::new(head_strict),
22483                op: BinOp::Or,
22484                rhs: Box::new(Expr::Binary {
22485                    lhs: Box::new(head_eq),
22486                    op: BinOp::And,
22487                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22488                }),
22489            }
22490        }
22491        let negated_in = if matches!(self.peek(), Token::Not)
22492            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22493        {
22494            self.advance();
22495            true
22496        } else {
22497            false
22498        };
22499        if matches!(self.peek(), Token::In) {
22500            self.advance();
22501            if !matches!(self.peek(), Token::LParen) {
22502                return Err(self.err(alloc::format!(
22503                    "expected '(' after row IN, got {:?}",
22504                    self.peek()
22505                )));
22506            }
22507            self.advance();
22508            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22509            // not a list of literal rows. Row-vs-list decomposes to
22510            // OR-of-AND above, but the subquery's rows are only known at
22511            // runtime, so keep it as a RowInSubquery node.
22512            if matches!(self.peek(), Token::Select) {
22513                let inner = self.parse_select_stmt()?;
22514                if !matches!(self.peek(), Token::RParen) {
22515                    return Err(self.err(alloc::format!(
22516                        "expected ')' after row IN-subquery, got {:?}",
22517                        self.peek()
22518                    )));
22519                }
22520                self.advance();
22521                let Statement::Select(s) = inner else {
22522                    unreachable!("parse_select_stmt always returns Statement::Select")
22523                };
22524                return Ok(Expr::RowInSubquery {
22525                    row,
22526                    subquery: Box::new(s),
22527                    negated: negated_in,
22528                });
22529            }
22530            let mut alternatives: Vec<Expr> = Vec::new();
22531            loop {
22532                // Optional ROW keyword before the paren row.
22533                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22534                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22535                {
22536                    self.advance();
22537                }
22538                if !matches!(self.peek(), Token::LParen) {
22539                    return Err(self.err(alloc::format!(
22540                        "expected '(' to open a row inside IN, got {:?}",
22541                        self.peek()
22542                    )));
22543                }
22544                self.advance();
22545                let mut rhs = alloc::vec![self.parse_expr(0)?];
22546                while matches!(self.peek(), Token::Comma) {
22547                    self.advance();
22548                    rhs.push(self.parse_expr(0)?);
22549                }
22550                if !matches!(self.peek(), Token::RParen) {
22551                    return Err(self.err(alloc::format!(
22552                        "expected ')' after row inside IN, got {:?}",
22553                        self.peek()
22554                    )));
22555                }
22556                self.advance();
22557                if rhs.len() != row.len() {
22558                    return Err(self.err(alloc::format!(
22559                        "row IN arity mismatch: left has {}, right has {}",
22560                        row.len(),
22561                        rhs.len()
22562                    )));
22563                }
22564                alternatives.push(row_eq(&row, &rhs));
22565                if matches!(self.peek(), Token::Comma) {
22566                    self.advance();
22567                    continue;
22568                }
22569                break;
22570            }
22571            if !matches!(self.peek(), Token::RParen) {
22572                return Err(self.err(alloc::format!(
22573                    "expected ')' to close row IN list, got {:?}",
22574                    self.peek()
22575                )));
22576            }
22577            self.advance();
22578            let mut it = alternatives.into_iter();
22579            let first = it.next().expect("IN list has at least one row");
22580            let combined = it.fold(first, |acc, e| Expr::Binary {
22581                lhs: Box::new(acc),
22582                op: BinOp::Or,
22583                rhs: Box::new(e),
22584            });
22585            return Ok(maybe_not(combined, negated_in));
22586        }
22587        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
22588        // two periods share at least one time point. Each pair is
22589        // normalised with least/greatest (PG accepts the endpoints
22590        // in either order), then lowered to the standard
22591        // `start1 < end2 AND start2 < end1` form.
22592        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
22593            if row.len() != 2 {
22594                return Err(self.err(alloc::format!(
22595                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
22596                    row.len()
22597                )));
22598            }
22599            self.advance();
22600            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22601                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22602            {
22603                self.advance();
22604            }
22605            if !matches!(self.peek(), Token::LParen) {
22606                return Err(self.err(alloc::format!(
22607                    "expected '(' after OVERLAPS, got {:?}",
22608                    self.peek()
22609                )));
22610            }
22611            self.advance();
22612            let r0 = self.parse_expr(0)?;
22613            if !matches!(self.peek(), Token::Comma) {
22614                return Err(self.err(alloc::format!(
22615                    "OVERLAPS needs (start, end) on the right, got {:?}",
22616                    self.peek()
22617                )));
22618            }
22619            self.advance();
22620            let r1 = self.parse_expr(0)?;
22621            if !matches!(self.peek(), Token::RParen) {
22622                return Err(self.err(alloc::format!(
22623                    "expected ')' after OVERLAPS pair, got {:?}",
22624                    self.peek()
22625                )));
22626            }
22627            self.advance();
22628            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
22629                name: String::from(name),
22630                args: alloc::vec![a.clone(), b.clone()],
22631            };
22632            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
22633                lhs: Box::new(lhs),
22634                op: BinOp::Lt,
22635                rhs: Box::new(rhs),
22636            };
22637            return Ok(Expr::Binary {
22638                lhs: Box::new(lt(
22639                    pair_fn("least", &row[0], &row[1]),
22640                    pair_fn("greatest", &r0, &r1),
22641                )),
22642                op: BinOp::And,
22643                rhs: Box::new(lt(
22644                    pair_fn("least", &r0, &r1),
22645                    pair_fn("greatest", &row[0], &row[1]),
22646                )),
22647            });
22648        }
22649        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
22650        // PG, `IS NULL` is true only when EVERY field is NULL, and
22651        // `IS NOT NULL` is true only when every field is non-NULL — the
22652        // latter is NOT the negation of the former (a mixed row is
22653        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
22654        // which reproduces exactly that all-fields semantics.
22655        if matches!(self.peek(), Token::Is) {
22656            self.advance();
22657            let negated = if matches!(self.peek(), Token::Not) {
22658                self.advance();
22659                true
22660            } else {
22661                false
22662            };
22663            if !matches!(self.peek(), Token::Null) {
22664                return Err(self.err(alloc::format!(
22665                    "expected NULL after row IS [NOT], got {:?}",
22666                    self.peek()
22667                )));
22668            }
22669            self.advance();
22670            let mut it = row.iter().map(|e| Expr::IsNull {
22671                expr: Box::new(e.clone()),
22672                negated,
22673            });
22674            let first = it.next().expect("row has at least two elements");
22675            return Ok(it.fold(first, |acc, e| Expr::Binary {
22676                lhs: Box::new(acc),
22677                op: BinOp::And,
22678                rhs: Box::new(e),
22679            }));
22680        }
22681        let op = match self.peek() {
22682            Token::Eq => BinOp::Eq,
22683            Token::NotEq => BinOp::NotEq,
22684            Token::Lt => BinOp::Lt,
22685            Token::LtEq => BinOp::LtEq,
22686            Token::Gt => BinOp::Gt,
22687            Token::GtEq => BinOp::GtEq,
22688            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
22689            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
22690            // constructor value, identical to the `ROW(a, b, …)` keyword form:
22691            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
22692            // (`::text`, `.field`) applies at the caller just as it does for the
22693            // ROW(...) node. All the comparison / predicate forms returned above.
22694            _ => {
22695                return Ok(Expr::FunctionCall {
22696                    name: String::from("row"),
22697                    args: row,
22698                });
22699            }
22700        };
22701        self.advance();
22702        // Optional ROW keyword before the paren row.
22703        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22704            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22705        {
22706            self.advance();
22707        }
22708        if !matches!(self.peek(), Token::LParen) {
22709            return Err(self.err(alloc::format!(
22710                "expected '(' to open the right-hand row, got {:?}",
22711                self.peek()
22712            )));
22713        }
22714        self.advance();
22715        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
22716        // subquery. Kept as a node (the subquery's row is a runtime value);
22717        // the literal-RHS form below still decomposes at parse time.
22718        if matches!(self.peek(), Token::Select) {
22719            let inner = self.parse_select_stmt()?;
22720            if !matches!(self.peek(), Token::RParen) {
22721                return Err(self.err(alloc::format!(
22722                    "expected ')' after row comparison subquery, got {:?}",
22723                    self.peek()
22724                )));
22725            }
22726            self.advance();
22727            let Statement::Select(s) = inner else {
22728                unreachable!("parse_select_stmt always returns Statement::Select")
22729            };
22730            return Ok(Expr::RowCmpSubquery {
22731                row,
22732                op,
22733                subquery: Box::new(s),
22734            });
22735        }
22736        let mut rhs = alloc::vec![self.parse_expr(0)?];
22737        while matches!(self.peek(), Token::Comma) {
22738            self.advance();
22739            rhs.push(self.parse_expr(0)?);
22740        }
22741        if !matches!(self.peek(), Token::RParen) {
22742            return Err(self.err(alloc::format!(
22743                "expected ')' after right-hand row, got {:?}",
22744                self.peek()
22745            )));
22746        }
22747        self.advance();
22748        if rhs.len() != row.len() {
22749            // v7.39 (round 239) — PG's wording (42601).
22750            return Err(self.err("unequal number of entries in row expressions".to_string()));
22751        }
22752        Ok(match op {
22753            BinOp::Eq => row_eq(&row, &rhs),
22754            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
22755            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
22756            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
22757            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
22758            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
22759            _ => unreachable!("op restricted above"),
22760        })
22761    }
22762
22763    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
22764    /// escape character becomes the matcher's default backslash:
22765    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
22766    /// → the char itself, and any pre-existing backslash escapes
22767    /// itself so it stays literal. Both operands must be string
22768    /// literals — a runtime pattern would need matcher support.
22769    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
22770        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
22771            (&pattern, &esc)
22772        else {
22773            return Err(
22774                "LIKE ... ESCAPE requires string-literal pattern and escape \
22775                 (runtime escape characters are not supported yet)"
22776                    .into(),
22777            );
22778        };
22779        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
22780        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
22781        // multi-character escape is an error.
22782        let esc_ch: Option<char> = {
22783            let mut ch_iter = e.chars();
22784            match (ch_iter.next(), ch_iter.next()) {
22785                (Some(c), None) => Some(c),
22786                (None, _) => None,
22787                (Some(_), Some(_)) => {
22788                    return Err(alloc::format!(
22789                        "ESCAPE must be a single character, got {e:?}"
22790                    ));
22791                }
22792            }
22793        };
22794        let mut out = String::with_capacity(p.len() + 4);
22795        let mut chars = p.chars();
22796        while let Some(c) = chars.next() {
22797            if Some(c) == esc_ch {
22798                match chars.next() {
22799                    // Escaped wildcard / escaped escape → keep the
22800                    // next char literal via backslash.
22801                    Some(next) => {
22802                        out.push('\\');
22803                        out.push(next);
22804                    }
22805                    None => {
22806                        return Err("LIKE pattern ends with the escape character".into());
22807                    }
22808                }
22809            } else if c == '\\' && esc_ch != Some('\\') {
22810                // A raw backslash is literal under a custom (or absent) escape
22811                // — escape it for the backslash-based matcher.
22812                out.push('\\');
22813                out.push('\\');
22814            } else {
22815                out.push(c);
22816            }
22817        }
22818        Ok(Expr::Literal(Literal::String(out)))
22819    }
22820
22821    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
22822    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
22823    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
22824    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
22825    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
22826    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
22827    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
22828    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
22829    /// array expression errors honestly rather than silently mismatching.
22830    fn try_like_any_all(
22831        &mut self,
22832        base: &Expr,
22833        negated: bool,
22834        case_insensitive: bool,
22835    ) -> Result<Option<Expr>, ParseError> {
22836        let is_any = match self.peek() {
22837            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
22838            Token::Ident(s)
22839                if s.eq_ignore_ascii_case("any")
22840                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22841            {
22842                true
22843            }
22844            _ => return Ok(None),
22845        };
22846        self.advance(); // ANY / ALL
22847        self.advance(); // '('
22848        let arr = self.parse_expr(0)?;
22849        if !matches!(self.peek(), Token::RParen) {
22850            return Err(self.err(format!(
22851                "expected ')' after LIKE {} argument, got {:?}",
22852                if is_any { "ANY" } else { "ALL" },
22853                self.peek()
22854            )));
22855        }
22856        self.advance(); // ')'
22857        let Expr::Array(items) = arr else {
22858            return Err(self.err(
22859                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
22860            ));
22861        };
22862        let mut clauses = items.into_iter().map(|p| Expr::Like {
22863            expr: Box::new(base.clone()),
22864            pattern: Box::new(p),
22865            negated,
22866            case_insensitive,
22867        });
22868        let Some(first) = clauses.next() else {
22869            // ANY(empty) = FALSE, ALL(empty) = TRUE.
22870            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
22871        };
22872        let op = if is_any { BinOp::Or } else { BinOp::And };
22873        let combined = clauses.fold(first, |acc, c| Expr::Binary {
22874            lhs: Box::new(acc),
22875            op,
22876            rhs: Box::new(c),
22877        });
22878        Ok(Some(combined))
22879    }
22880
22881    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
22882    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
22883    /// `AND` is not swallowed.
22884    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
22885        self.advance(); // BETWEEN
22886        // SYMMETRIC — the bounds may arrive in either order; both
22887        // orientations OR together. ASYMMETRIC is the default and
22888        // absorbs as noise.
22889        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
22890        {
22891            self.advance();
22892            true
22893        } else {
22894            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
22895                self.advance();
22896            }
22897            false
22898        };
22899        let low = self.parse_expr(6)?;
22900        if !matches!(self.peek(), Token::And) {
22901            return Err(self.err(format!(
22902                "expected AND after BETWEEN low bound, got {:?}",
22903                self.peek()
22904            )));
22905        }
22906        self.advance();
22907        let high = self.parse_expr(6)?;
22908        let target = Box::new(expr);
22909        let range = |lo: Expr, hi: Expr| Expr::Binary {
22910            lhs: Box::new(Expr::Binary {
22911                lhs: target.clone(),
22912                op: BinOp::GtEq,
22913                rhs: Box::new(lo),
22914            }),
22915            op: BinOp::And,
22916            rhs: Box::new(Expr::Binary {
22917                lhs: target.clone(),
22918                op: BinOp::LtEq,
22919                rhs: Box::new(hi),
22920            }),
22921        };
22922        let combined = if symmetric {
22923            Expr::Binary {
22924                lhs: Box::new(range(low.clone(), high.clone())),
22925                op: BinOp::Or,
22926                rhs: Box::new(range(high, low)),
22927            }
22928        } else {
22929            range(low, high)
22930        };
22931        Ok(maybe_not(combined, negated))
22932    }
22933
22934    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
22935    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
22936    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
22937    /// Caller already consumed the leading `WITH` ident.
22938    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
22939    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
22940    /// self-reference that appears more than once in a single term.
22941    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
22942        use crate::ast::{CteBody, SelectStatement};
22943        if !cte.recursive {
22944            return Ok(());
22945        }
22946        let CteBody::Select(body) = &cte.body else {
22947            return Ok(());
22948        };
22949        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
22950        // check the anchor and every peer term.
22951        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
22952        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
22953        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
22954            return Err(self.err(String::from(
22955                "ORDER BY in a recursive query is not implemented",
22956            )));
22957        }
22958        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
22959            return Err(self.err(String::from(
22960                "LIMIT in a recursive query is not implemented",
22961            )));
22962        }
22963        let self_refs = |s: &SelectStatement| -> usize {
22964            let Some(from) = &s.from else {
22965                return 0;
22966            };
22967            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
22968            for j in &from.joins {
22969                if j.table.name.eq_ignore_ascii_case(&cte.name) {
22970                    n += 1;
22971                }
22972            }
22973            n
22974        };
22975        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
22976            return Err(self.err(alloc::format!(
22977                "recursive reference to query \"{}\" must not appear more than once",
22978                cte.name
22979            )));
22980        }
22981        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
22982        // apply only when the body actually references itself (a non-self-
22983        // referencing CTE under WITH RECURSIVE may use any set-op shape).
22984        let anchor_refs = self_refs(body);
22985        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
22986        if anchor_refs > 0 || union_refs {
22987            // Shape: the top level must be UNION [ALL] arms only. A self-ref
22988            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
22989            // "does not have the form" error — SPG used to compute a value.
22990            if body.unions.is_empty()
22991                || body.unions.iter().any(|(k, _)| {
22992                    !matches!(
22993                        k,
22994                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
22995                    )
22996                })
22997            {
22998                return Err(self.err(alloc::format!(
22999                    "recursive query \"{}\" does not have the form non-recursive-term \
23000                     UNION [ALL] recursive-term",
23001                    cte.name
23002                )));
23003            }
23004            if anchor_refs > 0 {
23005                return Err(self.err(alloc::format!(
23006                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
23007                    cte.name
23008                )));
23009            }
23010        }
23011        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23012        for (_, u) in &body.unions {
23013            if self_refs(u) == 0 {
23014                continue;
23015            }
23016            // The self-reference must not sit on the nullable side of an outer
23017            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23018            if let Some(from) = &u.from {
23019                for (i, j) in from.joins.iter().enumerate() {
23020                    let left_has_self = is_self(&from.primary)
23021                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23022                    let violated = match j.kind {
23023                        crate::ast::JoinKind::Left => is_self(&j.table),
23024                        crate::ast::JoinKind::Right => left_has_self,
23025                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23026                        _ => false,
23027                    };
23028                    if violated {
23029                        return Err(self.err(alloc::format!(
23030                            "recursive reference to query \"{}\" must not appear within an outer join",
23031                            cte.name
23032                        )));
23033                    }
23034                }
23035            }
23036            // No aggregates at the top level of the recursive term (SPG used
23037            // to run them and surface a misleading downstream error).
23038            let mut items_and_having: Vec<&Expr> = Vec::new();
23039            for it in &u.items {
23040                if let crate::ast::SelectItem::Expr { expr, .. } = it {
23041                    items_and_having.push(expr);
23042                }
23043            }
23044            if let Some(h) = &u.having {
23045                items_and_having.push(h);
23046            }
23047            for e in items_and_having {
23048                if expr_has_toplevel_aggregate(e) {
23049                    return Err(self.err(String::from(
23050                        "aggregate functions are not allowed in a recursive query's recursive term",
23051                    )));
23052                }
23053            }
23054        }
23055        // A self-reference inside a sublink expression (EXISTS / IN / scalar
23056        // subquery) anywhere in the body is rejected; a plain FROM derived
23057        // table is legal in PG and untouched here.
23058        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23059        all_terms.extend(body.unions.iter().map(|(_, u)| u));
23060        for term in all_terms {
23061            if select_has_self_ref_in_sublink(term, &cte.name) {
23062                return Err(self.err(alloc::format!(
23063                    "recursive reference to query \"{}\" must not appear within a subquery",
23064                    cte.name
23065                )));
23066            }
23067        }
23068        Ok(())
23069    }
23070
23071    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23072    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23073    /// right after parse so the engine sees a plain recursive CTE with the
23074    /// tracking columns already projected. DEPTH FIRST and CYCLE are
23075    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23076    /// text-rendered rows can't provide, and errors honestly.
23077    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23078        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23079        if cte.search.is_none() && cte.cycle.is_none() {
23080            return Ok(());
23081        }
23082        let cte_name = cte.name.clone();
23083        let col_names = cte.column_overrides.clone();
23084        if col_names.is_empty() {
23085            return Err(
23086                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23087            );
23088        }
23089        let search = cte.search.take();
23090        let cycle = cte.cycle.take();
23091        let mut extra_cols: Vec<String> = Vec::new();
23092        let col_ref = |name: &str| {
23093            Expr::Column(ColumnName {
23094                qualifier: Some(cte_name.clone()),
23095                name: name.to_string(),
23096            })
23097        };
23098        // Position of a SEARCH/CYCLE column within the CTE's column list.
23099        let pos_of = |name: &str| -> Result<usize, ParseError> {
23100            col_names
23101                .iter()
23102                .position(|c| c.eq_ignore_ascii_case(name))
23103                .ok_or_else(|| {
23104                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23105                })
23106        };
23107        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23108            let mut args = Vec::with_capacity(positions.len());
23109            for &p in positions {
23110                match items.get(p) {
23111                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23112                    _ => {
23113                        return Err(self.err(
23114                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
23115                        ));
23116                    }
23117                }
23118            }
23119            Ok(Expr::FunctionCall {
23120                name: "row".into(),
23121                args,
23122            })
23123        };
23124        let CteBody::Select(body) = &mut cte.body else {
23125            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23126        };
23127        if body.unions.is_empty() {
23128            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23129        }
23130        let rec = body.unions.len() - 1; // recursive term = last UNION peer
23131
23132        if let Some(srch) = search {
23133            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23134            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23135            // no typed `record[]`, but element-wise array ORDER BY is correct
23136            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23137            // exactly onto a typed array: DEPTH is the root→node path
23138            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23139            // orders numerically (multi-digit keys included), matching PG.
23140            //
23141            // A multi-column BY would need a record[] to keep the per-node key
23142            // tuple orderable, which SPG can't express — error honestly there
23143            // rather than mis-order.
23144            if srch.by_columns.len() != 1 {
23145                return Err(self.err(
23146                    "SEARCH … BY with multiple columns needs typed record[] ordering \
23147                     SPG doesn't have yet; a single BY column is supported"
23148                        .into(),
23149                ));
23150            }
23151            let key_pos = pos_of(&srch.by_columns[0])?;
23152            let base_key = match body.items.get(key_pos) {
23153                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23154                _ => {
23155                    return Err(
23156                        self.err("SEARCH BY column maps to a non-expression select item".into())
23157                    );
23158                }
23159            };
23160            let rec_key = match body.unions[rec].1.items.get(key_pos) {
23161                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23162                _ => {
23163                    return Err(
23164                        self.err("SEARCH BY column maps to a non-expression select item".into())
23165                    );
23166                }
23167            };
23168            if srch.depth_first {
23169                // base: ARRAY[key]; rec: array_append(cte.set, key).
23170                body.items.push(SelectItem::Expr {
23171                    expr: Expr::Array(alloc::vec![base_key]),
23172                    alias: Some(srch.set_column.clone()),
23173                });
23174                body.unions[rec].1.items.push(SelectItem::Expr {
23175                    expr: Expr::FunctionCall {
23176                        name: "array_append".into(),
23177                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
23178                    },
23179                    alias: Some(srch.set_column.clone()),
23180                });
23181            } else {
23182                // BREADTH: [depth, key]; depth starts at 0 and increments. The
23183                // leading depth element dominates the element-wise comparison,
23184                // so shallower rows sort first, then by key — PG's (depth, key).
23185                body.items.push(SelectItem::Expr {
23186                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23187                    alias: Some(srch.set_column.clone()),
23188                });
23189                // rec depth = cte.set[1] + 1.
23190                let parent_depth = Expr::ArraySubscript {
23191                    target: Box::new(col_ref(&srch.set_column)),
23192                    index: Box::new(Expr::Literal(Literal::Integer(1))),
23193                };
23194                body.unions[rec].1.items.push(SelectItem::Expr {
23195                    expr: Expr::Array(alloc::vec![
23196                        Expr::Binary {
23197                            lhs: Box::new(parent_depth),
23198                            op: BinOp::Add,
23199                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23200                        },
23201                        rec_key,
23202                    ]),
23203                    alias: Some(srch.set_column.clone()),
23204                });
23205            }
23206            extra_cols.push(srch.set_column);
23207        }
23208
23209        if let Some(cyc) = cycle {
23210            let positions: Vec<usize> = cyc
23211                .columns
23212                .iter()
23213                .map(|c| pos_of(c))
23214                .collect::<Result<_, _>>()?;
23215            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23216            // cast it to text for the cycle path: membership only needs equality,
23217            // and the record text form gives SPG a TextArray path (SPG has no
23218            // typed record[] array). Cycle detection is unaffected.
23219            let base_row = Expr::Cast {
23220                expr: Box::new(row_of(&body.items, &positions)?),
23221                target: CastTarget::Text,
23222            };
23223            let rec_row = Expr::Cast {
23224                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23225                target: CastTarget::Text,
23226            };
23227            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23228            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23229            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23230            body.items.push(SelectItem::Expr {
23231                expr: Expr::Literal(dflt.clone()),
23232                alias: Some(cyc.mark_column.clone()),
23233            });
23234            body.items.push(SelectItem::Expr {
23235                expr: Expr::Array(alloc::vec![base_row]),
23236                alias: Some(cyc.path_column.clone()),
23237            });
23238            // rec mark: ROW(cols) already in the path → cycle.
23239            let hit = Expr::AnyAll {
23240                expr: Box::new(rec_row.clone()),
23241                op: BinOp::Eq,
23242                array: Box::new(col_ref(&cyc.path_column)),
23243                is_any: true,
23244            };
23245            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23246                Expr::Case {
23247                    operand: None,
23248                    branches: alloc::vec![(hit, Expr::Literal(mark))],
23249                    else_branch: Some(Box::new(Expr::Literal(dflt))),
23250                }
23251            } else {
23252                hit
23253            };
23254            body.unions[rec].1.items.push(SelectItem::Expr {
23255                expr: mark_expr,
23256                alias: Some(cyc.mark_column.clone()),
23257            });
23258            // rec path: array_append(cte.path, ROW(cols)).
23259            body.unions[rec].1.items.push(SelectItem::Expr {
23260                expr: Expr::FunctionCall {
23261                    name: "array_append".into(),
23262                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23263                },
23264                alias: Some(cyc.path_column.clone()),
23265            });
23266            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23267            let stop = Expr::Unary {
23268                op: UnOp::Not,
23269                expr: Box::new(col_ref(&cyc.mark_column)),
23270            };
23271            let w = &mut body.unions[rec].1.where_;
23272            *w = Some(match w.take() {
23273                Some(prev) => Expr::Binary {
23274                    lhs: Box::new(prev),
23275                    op: BinOp::And,
23276                    rhs: Box::new(stop),
23277                },
23278                None => stop,
23279            });
23280            extra_cols.push(cyc.mark_column);
23281            extra_cols.push(cyc.path_column);
23282        }
23283        cte.column_overrides.extend(extra_cols);
23284        Ok(())
23285    }
23286
23287    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23288    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23289    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23290        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23291            return Ok(None);
23292        }
23293        self.advance(); // SEARCH
23294        let depth_first = match self.peek() {
23295            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23296            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23297            other => {
23298                return Err(self.err(format!(
23299                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23300                )));
23301            }
23302        };
23303        self.advance();
23304        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23305            return Err(self.err(format!(
23306                "expected FIRST after SEARCH mode, got {:?}",
23307                self.peek()
23308            )));
23309        }
23310        self.advance();
23311        if !self.peek_is_by() {
23312            return Err(self.err(format!(
23313                "expected BY after SEARCH … FIRST, got {:?}",
23314                self.peek()
23315            )));
23316        }
23317        self.advance();
23318        let mut by_columns = alloc::vec![self.expect_ident_like()?];
23319        while matches!(self.peek(), Token::Comma) {
23320            self.advance();
23321            by_columns.push(self.expect_ident_like()?);
23322        }
23323        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23324            return Err(self.err(format!(
23325                "expected SET in SEARCH clause, got {:?}",
23326                self.peek()
23327            )));
23328        }
23329        self.advance();
23330        let set_column = self.expect_ident_like()?;
23331        Ok(Some(crate::ast::SearchClause {
23332            depth_first,
23333            by_columns,
23334            set_column,
23335        }))
23336    }
23337
23338    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23339    /// USING pathcol`. Returns None when the next token isn't CYCLE.
23340    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23341        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23342            return Ok(None);
23343        }
23344        self.advance(); // CYCLE
23345        let mut columns = alloc::vec![self.expect_ident_like()?];
23346        while matches!(self.peek(), Token::Comma) {
23347            self.advance();
23348            columns.push(self.expect_ident_like()?);
23349        }
23350        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23351            return Err(self.err(format!(
23352                "expected SET in CYCLE clause, got {:?}",
23353                self.peek()
23354            )));
23355        }
23356        self.advance();
23357        let mark_column = self.expect_ident_like()?;
23358        let mut mark_value = None;
23359        let mut default_value = None;
23360        if matches!(self.peek(), Token::To) {
23361            self.advance();
23362            mark_value = Some(self.parse_cycle_literal()?);
23363            if !matches!(self.peek(), Token::Default) {
23364                return Err(self.err(format!(
23365                    "expected DEFAULT after CYCLE … TO, got {:?}",
23366                    self.peek()
23367                )));
23368            }
23369            self.advance();
23370            default_value = Some(self.parse_cycle_literal()?);
23371        }
23372        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23373            return Err(self.err(format!(
23374                "expected USING in CYCLE clause, got {:?}",
23375                self.peek()
23376            )));
23377        }
23378        self.advance();
23379        let path_column = self.expect_ident_like()?;
23380        Ok(Some(crate::ast::CycleClause {
23381            columns,
23382            mark_column,
23383            mark_value,
23384            default_value,
23385            path_column,
23386        }))
23387    }
23388
23389    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23390    /// literal (string / bool / number) in PG.
23391    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23392        match self.parse_expr(0)? {
23393            Expr::Literal(l) => Ok(l),
23394            other => Err(self.err(format!(
23395                "CYCLE mark/default value must be a literal, got {other:?}"
23396            ))),
23397        }
23398    }
23399
23400    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23401        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23402        // Comes through as an identifier; consume it if present and
23403        // mark every CTE in the clause as recursive (PG semantics —
23404        // the flag is per-WITH, not per-CTE).
23405        let mut recursive = false;
23406        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23407            && s.eq_ignore_ascii_case("recursive")
23408        {
23409            self.advance();
23410            recursive = true;
23411        }
23412        let mut ctes = Vec::new();
23413        loop {
23414            let name = self.expect_ident_like()?;
23415            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23416            // PG uses these to rename the body's output columns; we
23417            // do the same below by overriding `columns[i].name`.
23418            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23419                self.advance();
23420                let mut names = Vec::new();
23421                loop {
23422                    names.push(self.expect_ident_like()?);
23423                    if matches!(self.peek(), Token::Comma) {
23424                        self.advance();
23425                        continue;
23426                    }
23427                    break;
23428                }
23429                if !matches!(self.peek(), Token::RParen) {
23430                    return Err(self.err(format!(
23431                        "expected ')' to close CTE column list, got {:?}",
23432                        self.peek()
23433                    )));
23434                }
23435                self.advance();
23436                names
23437            } else {
23438                Vec::new()
23439            };
23440            // AS is a reserved Token::As (used by SELECT-item / FROM
23441            // aliasing) — handle it specially rather than as a bare
23442            // ident.
23443            if !matches!(self.peek(), Token::As) {
23444                return Err(self.err(format!(
23445                    "expected AS after CTE name {name:?}, got {:?}",
23446                    self.peek()
23447                )));
23448            }
23449            self.advance();
23450            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23451            // MATERIALIZED` optimizer hints. SPG materialises every
23452            // CTE, so both spellings are accepted and absorbed.
23453            if matches!(self.peek(), Token::Not) {
23454                self.advance(); // NOT
23455                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23456                    if s.eq_ignore_ascii_case("materialized"))
23457                {
23458                    self.advance();
23459                } else {
23460                    return Err(self.err(format!(
23461                        "expected MATERIALIZED after AS NOT, got {:?}",
23462                        self.peek()
23463                    )));
23464                }
23465            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23466                if s.eq_ignore_ascii_case("materialized"))
23467            {
23468                self.advance();
23469            }
23470            if !matches!(self.peek(), Token::LParen) {
23471                return Err(self.err(format!(
23472                    "expected '(' after AS in WITH clause, got {:?}",
23473                    self.peek()
23474                )));
23475            }
23476            self.advance();
23477            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23478            // RETURNING) as the CTE body in addition to SELECT.
23479            // PG writable CTE semantics. UPDATE / DELETE come in as
23480            // bare Idents (lexer keeps SELECT / INSERT as reserved
23481            // tokens but treats the rest of DML as case-insensitive
23482            // idents).
23483            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23484            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23485            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23486            let body = match self.peek() {
23487                Token::Select => {
23488                    let inner = self.parse_select_stmt()?;
23489                    let Statement::Select(s) = inner else {
23490                        unreachable!("parse_select_stmt returns Select");
23491                    };
23492                    crate::ast::CteBody::Select(s)
23493                }
23494                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23495                // `SELECT * FROM t` this way and accepts it wherever a
23496                // SELECT goes, so the CTE body dispatch needs its own
23497                // arm: this match is keyed on the FIRST token, and
23498                // `Token::Table` fell through to a tail that then
23499                // rejected what it got. `parse_table_shorthand` has
23500                // returned a desugared SelectStatement since the
23501                // shorthand landed — only the routing was missing.
23502                // Round 868 found this by putting the shorthand in a
23503                // subquery; every earlier check used a top-level form.
23504                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23505                // `SELECT * FROM t` this way and accepts it wherever a
23506                // SELECT goes, so the CTE body dispatch needs its own
23507                // arm: this match is keyed on the FIRST token, and
23508                // `Token::Table` fell through to a tail that rejected
23509                // what it got. `parse_table_shorthand` has returned a
23510                // desugared SelectStatement since the shorthand landed —
23511                // only the routing was missing, here and in the derived
23512                // table's second-token gate. Round 868 found both by
23513                // putting the shorthand in a subquery; every earlier
23514                // check had used a top-level form.
23515                Token::Table
23516                    if matches!(
23517                        self.tokens.get(self.pos + 1),
23518                        Some(Token::Ident(_) | Token::QuotedIdent(_))
23519                    ) =>
23520                {
23521                    let mut head = self.parse_table_shorthand()?;
23522                    self.parse_setop_chain_into(&mut head)?;
23523                    self.parse_select_tail_into(&mut head)?;
23524                    crate::ast::CteBody::Select(head)
23525                }
23526                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23527                // WITH t(a) AS (VALUES (1), (2)) … lowers through
23528                // the shared rows helper onto a Select body.
23529                Token::Values => {
23530                    self.advance(); // VALUES
23531                    let mut head = self.parse_values_rows_body()?;
23532                    // A VALUES seed can head a set-operation chain —
23533                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23534                    // SELECT n+1 FROM t …). Attach any trailing
23535                    // UNION / INTERSECT / EXCEPT peers so the
23536                    // recursive-CTE body parses like the SELECT seed.
23537                    self.parse_setop_chain_into(&mut head)?;
23538                    crate::ast::CteBody::Select(head)
23539                }
23540                Token::Insert => {
23541                    let inner = self.parse_one_statement()?;
23542                    let Statement::Insert(s) = inner else {
23543                        unreachable!("Token::Insert routes to Insert");
23544                    };
23545                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23546                }
23547                _ if is_update_kw => {
23548                    let inner = self.parse_one_statement()?;
23549                    let Statement::Update(s) = inner else {
23550                        return Err(
23551                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23552                        );
23553                    };
23554                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23555                }
23556                _ if is_delete_kw => {
23557                    let inner = self.parse_one_statement()?;
23558                    let Statement::Delete(s) = inner else {
23559                        return Err(
23560                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23561                        );
23562                    };
23563                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23564                }
23565                // v7.39 (round 149) — PG 17 allows MERGE as a
23566                // data-modifying CTE body.
23567                _ if is_merge_kw => {
23568                    let inner = self.parse_one_statement()?;
23569                    let Statement::Merge(s) = inner else {
23570                        return Err(
23571                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23572                        );
23573                    };
23574                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23575                }
23576                // v7.39 (round 151) — a CTE body may itself be
23577                // WITH-headed (PG grammar: PreparableStmt carries its
23578                // own with_clause). The nested statement keeps its own
23579                // ctes; the modifying-CTE-at-top-level rule is enforced
23580                // at execution.
23581                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
23582                    self.advance(); // WITH
23583                    match self.parse_with_cte_then_select()? {
23584                        Statement::Select(s) => crate::ast::CteBody::Select(s),
23585                        Statement::Insert(s) => {
23586                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23587                        }
23588                        Statement::Update(s) => {
23589                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23590                        }
23591                        Statement::Delete(s) => {
23592                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23593                        }
23594                        Statement::Merge(s) => {
23595                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23596                        }
23597
23598                        other => {
23599                            return Err(self.err(format!(
23600                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23601                            )));
23602                        }
23603                    }
23604                }
23605                other => {
23606                    return Err(self.err(format!(
23607                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23608                    )));
23609                }
23610            };
23611            if !matches!(self.peek(), Token::RParen) {
23612                return Err(self.err(format!(
23613                    "expected ')' after CTE body, got {:?}",
23614                    self.peek()
23615                )));
23616            }
23617            self.advance();
23618            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
23619            // CTE, desugared into extra body columns by the engine.
23620            let search = self.parse_cte_search_clause()?;
23621            let cycle = self.parse_cte_cycle_clause()?;
23622            let mut cte = crate::ast::Cte {
23623                name,
23624                body,
23625                recursive,
23626                column_overrides,
23627                search,
23628                cycle,
23629            };
23630            self.validate_recursive_cte(&cte)?;
23631            self.desugar_cte_search_cycle(&mut cte)?;
23632            ctes.push(cte);
23633            if matches!(self.peek(), Token::Comma) {
23634                self.advance();
23635                continue;
23636            }
23637            break;
23638        }
23639        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
23640        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
23641        // the parsed CTEs to whichever statement the body produces.
23642        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23643        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23644        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23645        match self.peek() {
23646            Token::Select => {
23647                let body_stmt = self.parse_select_stmt()?;
23648                let Statement::Select(mut body) = body_stmt else {
23649                    unreachable!()
23650                };
23651                body.ctes = ctes;
23652                Ok(Statement::Select(body))
23653            }
23654            Token::Insert => {
23655                let body_stmt = self.parse_one_statement()?;
23656                let Statement::Insert(mut body) = body_stmt else {
23657                    unreachable!()
23658                };
23659                body.ctes = ctes;
23660                Ok(Statement::Insert(body))
23661            }
23662            _ if outer_is_update => {
23663                let body_stmt = self.parse_one_statement()?;
23664                let Statement::Update(mut body) = body_stmt else {
23665                    return Err(self.err(format!("expected UPDATE after WITH clause")));
23666                };
23667                body.ctes = ctes;
23668                Ok(Statement::Update(body))
23669            }
23670            _ if outer_is_delete => {
23671                let body_stmt = self.parse_one_statement()?;
23672                let Statement::Delete(mut body) = body_stmt else {
23673                    return Err(self.err(format!("expected DELETE after WITH clause")));
23674                };
23675                body.ctes = ctes;
23676                Ok(Statement::Delete(body))
23677            }
23678            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
23679            // WITH RECURSIVE is rejected with PG's exact message
23680            // (parse analysis, transformWithClause).
23681            _ if outer_is_merge => {
23682                if recursive {
23683                    return Err(self.err(String::from(
23684                        "WITH RECURSIVE is not supported for MERGE statement",
23685                    )));
23686                }
23687                let body_stmt = self.parse_one_statement()?;
23688                let Statement::Merge(mut body) = body_stmt else {
23689                    return Err(self.err(format!("expected MERGE after WITH clause")));
23690                };
23691                body.ctes = ctes;
23692                Ok(Statement::Merge(body))
23693            }
23694            other => Err(self.err(format!(
23695                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
23696            ))),
23697        }
23698    }
23699
23700    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
23701    /// already consumed the leading `EXISTS` ident via
23702    /// `self.advance()`.
23703    /// v7.13.0 — parse the rest of a `CASE … END` expression after
23704    /// the leading `CASE` ident has been consumed (mailrs round-5
23705    /// G9). Supports both the searched form
23706    /// (`CASE WHEN cond THEN val …`) and the simple form
23707    /// (`CASE operand WHEN val THEN val …`).
23708    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
23709        // Disambiguate searched vs simple form: if the next token
23710        // is `WHEN`, we're in the searched form. Otherwise the
23711        // intervening expression is the operand.
23712        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
23713            None
23714        } else {
23715            Some(Box::new(self.parse_expr(0)?))
23716        };
23717        let mut branches: Vec<(Expr, Expr)> = Vec::new();
23718        loop {
23719            match self.peek() {
23720                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
23721                    self.advance();
23722                    let cond = self.parse_expr(0)?;
23723                    match self.peek() {
23724                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
23725                            self.advance();
23726                        }
23727                        other => {
23728                            return Err(self.err(alloc::format!(
23729                                "expected THEN after CASE WHEN <expr>, got {other:?}"
23730                            )));
23731                        }
23732                    }
23733                    let value = self.parse_expr(0)?;
23734                    branches.push((cond, value));
23735                }
23736                _ => break,
23737            }
23738        }
23739        if branches.is_empty() {
23740            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
23741        }
23742        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
23743        {
23744            self.advance();
23745            Some(Box::new(self.parse_expr(0)?))
23746        } else {
23747            None
23748        };
23749        match self.peek() {
23750            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
23751                self.advance();
23752            }
23753            other => {
23754                return Err(self.err(alloc::format!(
23755                    "expected END to close CASE expression, got {other:?}"
23756                )));
23757            }
23758        }
23759        Ok(Expr::Case {
23760            operand,
23761            branches,
23762            else_branch,
23763        })
23764    }
23765
23766    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
23767    /// query-source position (EXISTS / IN / INSERT source / CTE body /
23768    /// view body). Caller consumed the WITH keyword. Only a SELECT
23769    /// outer is grammatical here; the data-modifying-CTE-at-top-level
23770    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
23771    /// maps correctly.
23772    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23773        let inner = self.parse_with_cte_then_select()?;
23774        match inner {
23775            Statement::Select(s) => Ok(s),
23776            other => Err(self.err(format!(
23777                "expected SELECT after WITH in a subquery, got {other:?}"
23778            ))),
23779        }
23780    }
23781
23782    /// True when the next token is the (unquoted) WITH keyword. WITH is
23783    /// reserved in PG, so a bare `with` can never be a column reference
23784    /// in these positions; a quoted `"with"` stays an identifier.
23785    fn peek_is_with_kw(&self) -> bool {
23786        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
23787    }
23788
23789    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
23790    /// `#[inline(never)]` keeps the large SelectStatement temporaries
23791    /// off parse_expr's recursive frame (the nesting-budget stack
23792    /// cliff — see the round-153 gate regression).
23793    #[inline(never)]
23794    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23795        if self.peek_is_with_kw() {
23796            self.advance();
23797            self.parse_nested_with_select()
23798        } else {
23799            match self.parse_select_stmt()? {
23800                Statement::Select(s) => Ok(s),
23801                other => Err(self.err(alloc::format!(
23802                    "expected SELECT inside ANY/ALL, got {other:?}"
23803                ))),
23804            }
23805        }
23806    }
23807
23808    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
23809        if !matches!(self.peek(), Token::LParen) {
23810            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
23811        }
23812        self.advance();
23813        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
23814        let s = if self.peek_is_with_kw() {
23815            self.advance();
23816            self.parse_nested_with_select()?
23817        } else {
23818            let inner = self.parse_select_stmt()?;
23819            let Statement::Select(s) = inner else {
23820                unreachable!("parse_select_stmt returns Select")
23821            };
23822            s
23823        };
23824        if !matches!(self.peek(), Token::RParen) {
23825            return Err(self.err(format!(
23826                "expected ')' after EXISTS-subquery, got {:?}",
23827                self.peek()
23828            )));
23829        }
23830        self.advance();
23831        Ok(Expr::Exists {
23832            subquery: Box::new(s),
23833            negated,
23834        })
23835    }
23836
23837    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23838        self.advance(); // IN
23839        if !matches!(self.peek(), Token::LParen) {
23840            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
23841        }
23842        self.advance();
23843        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
23844        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
23845        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
23846            let s = if self.peek_is_with_kw() {
23847                self.advance();
23848                self.parse_nested_with_select()?
23849            } else {
23850                let inner = self.parse_select_stmt()?;
23851                let Statement::Select(s) = inner else {
23852                    unreachable!("parse_select_stmt always returns Statement::Select")
23853                };
23854                s
23855            };
23856            if !matches!(self.peek(), Token::RParen) {
23857                return Err(self.err(format!(
23858                    "expected ')' after IN-subquery, got {:?}",
23859                    self.peek()
23860                )));
23861            }
23862            self.advance();
23863            return Ok(Expr::InSubquery {
23864                expr: Box::new(expr),
23865                subquery: Box::new(s),
23866                negated,
23867            });
23868        }
23869        let mut elements = Vec::new();
23870        if !matches!(self.peek(), Token::RParen) {
23871            loop {
23872                elements.push(self.parse_expr(0)?);
23873                match self.peek() {
23874                    Token::Comma => {
23875                        self.advance();
23876                    }
23877                    Token::RParen => break,
23878                    other => {
23879                        return Err(
23880                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
23881                        );
23882                    }
23883                }
23884            }
23885        }
23886        self.advance(); // ')'
23887        // v7.30.2 (mailrs round-25) — flat InList node instead of a
23888        // left-deep OR-Eq chain: chain depth scaled with the element
23889        // count and overflowed the stack (eval + drop are recursive).
23890        if elements.is_empty() {
23891            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
23892        }
23893        Ok(Expr::InList {
23894            expr: Box::new(expr),
23895            list: elements,
23896            negated,
23897        })
23898    }
23899
23900    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
23901    /// already consumed by the caller. Elements must be numeric literals
23902    /// (with optional unary `-`); any compound expression is rejected at
23903    /// parse time so the runtime never needs to evaluate inside a vector.
23904    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
23905    /// has already consumed the `EXTRACT` token before calling us —
23906    /// we pick up at the opening `(`.
23907    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
23908    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
23909    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
23910    /// per-column OR-fold of
23911    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
23912    /// term)` so the existing FTS evaluator handles semantics.
23913    ///
23914    /// The mode modifier is accepted-and-ignored at v7.17 — all
23915    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
23916    /// mode operators (`+foo -bar`) would need their own parser
23917    /// (Phase 2.2c); customers who hit them today already get a
23918    /// correct lexeme-match against the bare term, only without
23919    /// the +/- precedence the customer asked for.
23920    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
23921        // Already at `MATCH`-consumed position; the dispatcher
23922        // confirmed the next token is `(`.
23923        if !matches!(self.peek(), Token::LParen) {
23924            return Err(self.err(alloc::format!(
23925                "expected '(' after MATCH, got {:?}",
23926                self.peek()
23927            )));
23928        }
23929        self.advance();
23930        let mut cols: Vec<Expr> = Vec::new();
23931        loop {
23932            cols.push(self.parse_expr(0)?);
23933            match self.peek() {
23934                Token::Comma => {
23935                    self.advance();
23936                }
23937                Token::RParen => break,
23938                other => {
23939                    return Err(self.err(alloc::format!(
23940                        "expected ',' or ')' in MATCH column list, got {other:?}"
23941                    )));
23942                }
23943            }
23944        }
23945        self.advance(); // ')'
23946        // Expect AGAINST.
23947        match self.peek() {
23948            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
23949                self.advance();
23950            }
23951            other => {
23952                return Err(self.err(alloc::format!(
23953                    "expected AGAINST after MATCH column list, got {other:?}"
23954                )));
23955            }
23956        }
23957        if !matches!(self.peek(), Token::LParen) {
23958            return Err(self.err(alloc::format!(
23959                "expected '(' after AGAINST, got {:?}",
23960                self.peek()
23961            )));
23962        }
23963        self.advance();
23964        // Read AGAINST's argument as a single primary token —
23965        // string literal, placeholder, or column-ref ident. We
23966        // can't call `parse_expr` / `parse_unary` here because
23967        // the postfix chain inside `parse_atom` would greedily
23968        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
23969        // and fail at "expected '(' after IN". Customers always
23970        // write a literal or bound parameter in AGAINST, so this
23971        // restriction is non-blocking; the error path explains
23972        // the limit if a more complex expression shows up.
23973        let term = match self.advance() {
23974            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
23975            Token::Placeholder(n) => Expr::Placeholder(n),
23976            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
23977                qualifier: None,
23978                name: s,
23979            }),
23980            other => {
23981                return Err(self.err(alloc::format!(
23982                    "MATCH ... AGAINST(<term>) expects a string literal, \
23983                     bound parameter, or column ref, got {other:?}"
23984                )));
23985            }
23986        };
23987        // Optional mode tail — accept-and-ignore at v7.17:
23988        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
23989        //   IN BOOLEAN MODE
23990        //   WITH QUERY EXPANSION
23991        loop {
23992            match self.peek() {
23993                // IN lexes as a reserved Token::In, not an ident,
23994                // so it gets its own arm.
23995                Token::In => {
23996                    self.advance();
23997                }
23998                Token::Ident(s) | Token::QuotedIdent(s)
23999                    if s.eq_ignore_ascii_case("natural")
24000                        || s.eq_ignore_ascii_case("language")
24001                        || s.eq_ignore_ascii_case("boolean")
24002                        || s.eq_ignore_ascii_case("mode")
24003                        || s.eq_ignore_ascii_case("with")
24004                        || s.eq_ignore_ascii_case("query")
24005                        || s.eq_ignore_ascii_case("expansion") =>
24006                {
24007                    self.advance();
24008                }
24009                _ => break,
24010            }
24011        }
24012        if !matches!(self.peek(), Token::RParen) {
24013            return Err(self.err(alloc::format!(
24014                "expected ')' to close AGAINST, got {:?}",
24015                self.peek()
24016            )));
24017        }
24018        self.advance();
24019        // Build per-column `to_tsvector('simple', col) @@
24020        // plainto_tsquery('simple', term)` and OR-fold.
24021        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24022        let plainto = Expr::FunctionCall {
24023            name: String::from("plainto_tsquery"),
24024            args: alloc::vec![simple_lit(), term.clone()],
24025        };
24026        let mut folded: Option<Expr> = None;
24027        for col in cols {
24028            let to_tsv = Expr::FunctionCall {
24029                name: String::from("to_tsvector"),
24030                args: alloc::vec![simple_lit(), col],
24031            };
24032            let leaf = Expr::Binary {
24033                lhs: Box::new(to_tsv),
24034                op: crate::ast::BinOp::TsMatch,
24035                rhs: Box::new(plainto.clone()),
24036            };
24037            folded = Some(match folded {
24038                None => leaf,
24039                Some(prev) => Expr::Binary {
24040                    lhs: Box::new(prev),
24041                    op: crate::ast::BinOp::Or,
24042                    rhs: Box::new(leaf),
24043                },
24044            });
24045        }
24046        match folded {
24047            Some(e) => Ok(e),
24048            None => Err(self.err(String::from(
24049                "MATCH(...) AGAINST(...) requires at least one column",
24050            ))),
24051        }
24052    }
24053
24054    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24055        if !matches!(self.peek(), Token::LParen) {
24056            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24057        }
24058        self.advance();
24059        let field_name = self.expect_ident_like()?;
24060        let field = match field_name.to_ascii_lowercase().as_str() {
24061            // PG accepts the plural spellings (years/months/…/millenniums) as
24062            // aliases for the singular fields — its datetime unit table has both.
24063            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24064            "year" | "years" => ExtractField::Year,
24065            "month" | "months" => ExtractField::Month,
24066            "day" | "days" => ExtractField::Day,
24067            "hour" | "hours" => ExtractField::Hour,
24068            "minute" | "minutes" => ExtractField::Minute,
24069            "second" | "seconds" => ExtractField::Second,
24070            "microsecond" | "microseconds" => ExtractField::Microsecond,
24071            "epoch" => ExtractField::Epoch,
24072            "dow" => ExtractField::Dow,
24073            "isodow" => ExtractField::Isodow,
24074            "doy" => ExtractField::Doy,
24075            "week" | "weeks" => ExtractField::Week,
24076            "isoyear" => ExtractField::Isoyear,
24077            "quarter" => ExtractField::Quarter,
24078            "decade" | "decades" => ExtractField::Decade,
24079            "century" | "centuries" => ExtractField::Century,
24080            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24081            "julian" => ExtractField::Julian,
24082            "millisecond" | "milliseconds" => ExtractField::Millisecond,
24083            "timezone" => ExtractField::Timezone,
24084            "timezone_hour" => ExtractField::TimezoneHour,
24085            "timezone_minute" => ExtractField::TimezoneMinute,
24086            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24087            // reports an unknown one with the source type (22023); carry the
24088            // raw name so eval can word it.
24089            other => ExtractField::Other(alloc::string::String::from(other)),
24090        };
24091        if !matches!(self.peek(), Token::From) {
24092            return Err(self.err(format!(
24093                "expected FROM after EXTRACT field, got {:?}",
24094                self.peek()
24095            )));
24096        }
24097        self.advance();
24098        let source = self.parse_expr(0)?;
24099        if !matches!(self.peek(), Token::RParen) {
24100            return Err(self.err(format!(
24101                "expected ')' to close EXTRACT, got {:?}",
24102                self.peek()
24103            )));
24104        }
24105        self.advance();
24106        Ok(Expr::Extract {
24107            field,
24108            source: Box::new(source),
24109        })
24110    }
24111
24112    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24113    /// is already consumed; we expect a single string literal next and
24114    /// resolve it into `Literal::Interval` at parse time so the engine
24115    /// never has to re-tokenise inside the string.
24116    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24117    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24118    /// is the SQL-standard form and is left to the path below.
24119    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24120        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24121        let (offset, sign) = match self.peek() {
24122            Token::Minus => (1, "-"),
24123            _ => (0, ""),
24124        };
24125        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24126            return None;
24127        };
24128        self.tokens
24129            .get(self.pos + offset + 1)
24130            .filter(|t| mysql_interval_unit(t).is_some())?;
24131        Some((alloc::format!("{sign}{n}"), offset + 1))
24132    }
24133
24134    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24135    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24136    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24137    ///
24138    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24139    /// this by parsing the group and then restoring `self.pos` — which could
24140    /// never have worked, because `advance()` DESTROYS the token it returns
24141    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24142    /// inert only because both branches errored back then.
24143    fn interval_paren_is_quantity(&self) -> bool {
24144        let mut depth = 0usize;
24145        let mut saw_top_level_comma = false;
24146        let mut i = self.pos;
24147        while let Some(tok) = self.tokens.get(i) {
24148            match tok {
24149                Token::LParen => depth += 1,
24150                Token::RParen => {
24151                    depth = depth.saturating_sub(1);
24152                    if depth == 0 {
24153                        return !saw_top_level_comma
24154                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24155                                .is_some();
24156                    }
24157                }
24158                // A comma directly inside the outermost parens means the
24159                // argument list of the INTERVAL() function.
24160                Token::Comma if depth == 1 => saw_top_level_comma = true,
24161                Token::Eof => return false,
24162                _ => {}
24163            }
24164            i += 1;
24165        }
24166        false
24167    }
24168
24169    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24170        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24171        // (the index of the last Ni ≤ N), distinct from the interval literal.
24172        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24173        // is decided by a non-destructive lookahead (round 422) before either
24174        // branch consumes anything. MySQL only.
24175        if self.mysql_dialect
24176            && matches!(self.peek(), Token::LParen)
24177            && !self.interval_paren_is_quantity()
24178        {
24179            self.advance(); // (
24180            let mut args = Vec::new();
24181            if !matches!(self.peek(), Token::RParen) {
24182                loop {
24183                    args.push(self.parse_expr(0)?);
24184                    if matches!(self.peek(), Token::Comma) {
24185                        self.advance();
24186                        continue;
24187                    }
24188                    break;
24189                }
24190            }
24191            if !matches!(self.peek(), Token::RParen) {
24192                return Err(self.err(alloc::format!(
24193                    "expected ')' after INTERVAL() arguments, got {:?}",
24194                    self.peek()
24195                )));
24196            }
24197            self.advance(); // )
24198            return Ok(Expr::FunctionCall {
24199                name: alloc::string::String::from("interval"),
24200                args,
24201            });
24202        }
24203        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24204        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24205        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24206        // writes every date arithmetic there is, and it did not parse at
24207        // all. PG rejects the unquoted form outright (`syntax error at or
24208        // near "1"`, measured), so it is taken only in the MySQL dialect —
24209        // PG's own `INTERVAL '1' DAY` is untouched below.
24210        if self.mysql_dialect
24211            && let Some((text, consume)) = self.peek_unquoted_interval_count()
24212        {
24213            for _ in 0..consume {
24214                self.advance(); // the optional `-` and the number
24215            }
24216            let Some(unit) = mysql_interval_unit(self.peek()) else {
24217                return Err(self.err(alloc::format!(
24218                    "expected an interval unit after INTERVAL {text}, got {:?}",
24219                    self.peek()
24220                )));
24221            };
24222            self.advance(); // the unit
24223            let (months, days, micros) = scale_mysql_interval(&text, unit)
24224                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24225            return Ok(Expr::Literal(Literal::Interval {
24226                months,
24227                days,
24228                micros,
24229                // The canonical rendering, so Display round-trips into a
24230                // form both dialects read back.
24231                text: alloc::format!("{text} {unit}"),
24232            }));
24233        }
24234        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24235        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24236        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24237        // Those cannot fold into a compile-time `Literal::Interval`, so they
24238        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24239        // builtin, which builds the value at run time (and yields NULL for a
24240        // NULL quantity, as MariaDB does). The literal path above still folds
24241        // the constant case — it is cheaper and round-trips through Display.
24242        //
24243        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24244        // MySQL's quoted spelling) keep the qualifier path below.
24245        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24246            let qty = self.parse_expr(0)?;
24247            let Some(unit) = mysql_interval_unit(self.peek()) else {
24248                return Err(self.err(alloc::format!(
24249                    "expected an interval unit after INTERVAL <expr>, got {:?}",
24250                    self.peek()
24251                )));
24252            };
24253            self.advance(); // the unit
24254            return Ok(make_interval_call(qty, unit));
24255        }
24256        let tok = self.advance();
24257        let Token::String(text) = tok else {
24258            return Err(self.err(format!(
24259                "expected string literal after INTERVAL, got {tok:?}"
24260            )));
24261        };
24262        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24263        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24264        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24265        // bare number means and the leading/trailing precision.
24266        let field1 = interval_field_of(self.peek());
24267        let qualifier = if let Some(f1) = field1 {
24268            self.advance();
24269            let f2 = if matches!(self.peek(), Token::To) {
24270                self.advance();
24271                let Some(f) = interval_field_of(self.peek()) else {
24272                    return Err(self.err(format!(
24273                        "expected an interval field after TO, got {:?}",
24274                        self.peek()
24275                    )));
24276                };
24277                self.advance();
24278                Some(f)
24279            } else {
24280                None
24281            };
24282            Some((f1, f2))
24283        } else {
24284            None
24285        };
24286        let (months, days, micros) = match qualifier {
24287            Some(q) => interpret_qualified_interval(&text, q),
24288            None => parse_interval_text(&text),
24289        }
24290        .ok_or_else(|| ParseError {
24291            message: format!(
24292                "cannot parse INTERVAL {text:?}; \
24293                     expected `<n> <unit> [<n> <unit> ...]` with units \
24294                     microsecond[s], millisecond[s], second[s], minute[s], \
24295                     hour[s], day[s], week[s], month[s], year[s]"
24296            ),
24297            token_pos: self.consumed_pos(),
24298        })?;
24299        Ok(Expr::Literal(Literal::Interval {
24300            months,
24301            days,
24302            micros,
24303            text,
24304        }))
24305    }
24306
24307    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24308    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24309    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24310    /// than a pgvector literal.
24311    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24312        self.advance(); // consume `[`
24313        let mut items: Vec<Expr> = Vec::new();
24314        if !matches!(self.peek(), Token::RBracket) {
24315            loop {
24316                if matches!(self.peek(), Token::LBracket) {
24317                    items.push(self.parse_array_bracket_body()?);
24318                } else {
24319                    items.push(self.parse_expr(0)?);
24320                }
24321                match self.peek() {
24322                    Token::Comma => {
24323                        self.advance();
24324                    }
24325                    Token::RBracket => break,
24326                    other => {
24327                        return Err(self.err(alloc::format!(
24328                            "expected ',' or ']' in array literal, got {other:?}"
24329                        )));
24330                    }
24331                }
24332            }
24333        }
24334        self.advance(); // consume `]`
24335        Ok(Expr::Array(items))
24336    }
24337
24338    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24339        let mut elems = Vec::new();
24340        if matches!(self.peek(), Token::RBracket) {
24341            self.advance();
24342            return Ok(Expr::Literal(Literal::Vector(elems)));
24343        }
24344        loop {
24345            let e = self.parse_expr(0)?;
24346            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24347                message: format!("vector element must be a numeric literal, got {e:?}"),
24348                token_pos: self.pos,
24349            })?;
24350            elems.push(x);
24351            match self.peek() {
24352                Token::Comma => {
24353                    self.advance();
24354                }
24355                Token::RBracket => {
24356                    self.advance();
24357                    break;
24358                }
24359                other => {
24360                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24361                }
24362            }
24363        }
24364        Ok(Expr::Literal(Literal::Vector(elems)))
24365    }
24366
24367    /// Atom that started with an identifier: could be `t.col`, `col`, or
24368    /// `func(arg, ...)`. Detect each shape by looking at the next token.
24369    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24370    /// [, ...])`. Caller has already consumed `OVER`. Either clause
24371    /// is optional; an empty `()` is also legal (PG semantics).
24372    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24373    /// modifier between `name(args)` and `OVER (...)`. Default is
24374    /// `Respect`. Unrecognised idents leave the stream unchanged.
24375    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24376        let Token::Ident(s) = self.peek().clone() else {
24377            return NullTreatment::Respect;
24378        };
24379        let is_ignore = s.eq_ignore_ascii_case("ignore");
24380        let is_respect = s.eq_ignore_ascii_case("respect");
24381        if !is_ignore && !is_respect {
24382            return NullTreatment::Respect;
24383        }
24384        // Lookahead for NULLS — only consume both tokens together.
24385        // pos+1 must hold a "nulls" ident.
24386        if self.pos + 1 < self.tokens.len()
24387            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24388            && s2.eq_ignore_ascii_case("nulls")
24389        {
24390            self.advance();
24391            self.advance();
24392            return if is_ignore {
24393                NullTreatment::Ignore
24394            } else {
24395                NullTreatment::Respect
24396            };
24397        }
24398        NullTreatment::Respect
24399    }
24400
24401    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24402    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24403    /// (same shape as the `OVER` tail). Consumes the whole clause and
24404    /// returns the predicate; returns `None` when no `FILTER` follows.
24405    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24406        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24407            return Ok(None);
24408        };
24409        if !s.eq_ignore_ascii_case("filter") {
24410            return Ok(None);
24411        }
24412        self.advance(); // FILTER
24413        if !matches!(self.peek(), Token::LParen) {
24414            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24415        }
24416        self.advance(); // (
24417        if !matches!(self.peek(), Token::Where) {
24418            return Err(self.err(format!(
24419                "expected WHERE inside FILTER (...), got {:?}",
24420                self.peek()
24421            )));
24422        }
24423        self.advance(); // WHERE
24424        let cond = self.parse_expr(0)?;
24425        if !matches!(self.peek(), Token::RParen) {
24426            return Err(self.err(format!(
24427                "expected ')' to close FILTER (WHERE ...), got {:?}",
24428                self.peek()
24429            )));
24430        }
24431        self.advance(); // )
24432        Ok(Some(Box::new(cond)))
24433    }
24434
24435    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24436    /// the separator as the aggregate's second argument, which is the
24437    /// shape `string_agg` already takes. Returns whether one was there.
24438    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24439        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24440            return Ok(false);
24441        }
24442        self.advance();
24443        let Token::String(sep) = self.peek().clone() else {
24444            return Err(self.err(alloc::format!(
24445                "expected a string literal after SEPARATOR, got {:?}",
24446                self.peek()
24447            )));
24448        };
24449        self.advance();
24450        args.push(Expr::Literal(Literal::String(sep)));
24451        Ok(true)
24452    }
24453
24454    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24455    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24456    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24457    /// keys, or an empty vec when no `WITHIN GROUP` follows.
24458    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24459        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24460            return Ok(Vec::new());
24461        };
24462        if !s.eq_ignore_ascii_case("within") {
24463            return Ok(Vec::new());
24464        }
24465        self.advance(); // WITHIN
24466        if !matches!(self.peek(), Token::Group) {
24467            return Err(self.err(format!(
24468                "expected GROUP after WITHIN, got {:?}",
24469                self.peek()
24470            )));
24471        }
24472        self.advance(); // GROUP
24473        if !matches!(self.peek(), Token::LParen) {
24474            return Err(self.err(format!(
24475                "expected '(' after WITHIN GROUP, got {:?}",
24476                self.peek()
24477            )));
24478        }
24479        self.advance(); // (
24480        if !matches!(self.peek(), Token::Order) {
24481            return Err(self.err(format!(
24482                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24483                self.peek()
24484            )));
24485        }
24486        self.advance(); // ORDER
24487        if !self.peek_is_by() {
24488            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24489        }
24490        self.advance(); // BY
24491        let mut keys: Vec<OrderBy> = Vec::new();
24492        loop {
24493            // v7.39 (round 691) — save/restore, the discipline this parser
24494            // already uses around `pending_sample_preds`, so a subquery inside
24495            // a key neither inherits nor leaks the channel.
24496            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24497            let saved_coll = self.order_key_collation.take();
24498            let parsed = self.parse_expr(0);
24499            self.in_order_by_key = saved_flag;
24500            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24501            let expr = parsed?;
24502            let desc = if matches!(self.peek(), Token::Desc) {
24503                self.advance();
24504                true
24505            } else if matches!(self.peek(), Token::Asc) {
24506                self.advance();
24507                false
24508            } else {
24509                false
24510            };
24511            let nulls_first = self.parse_optional_nulls_placement()?;
24512            keys.push(OrderBy {
24513                expr,
24514                desc,
24515                nulls_first,
24516                collation,
24517            });
24518            if matches!(self.peek(), Token::Comma) {
24519                self.advance();
24520            } else {
24521                break;
24522            }
24523        }
24524        if !matches!(self.peek(), Token::RParen) {
24525            return Err(self.err(format!(
24526                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24527                self.peek()
24528            )));
24529        }
24530        self.advance(); // )
24531        Ok(keys)
24532    }
24533
24534    /// No frame clause is supported.
24535    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24536    fn parse_over_clause(
24537        &mut self,
24538    ) -> Result<
24539        (
24540            Vec<Expr>,
24541            Vec<(Expr, bool, Option<bool>)>,
24542            Option<WindowFrame>,
24543        ),
24544        ParseError,
24545    > {
24546        // `OVER w` — a named-window reference. The WINDOW clause
24547        // parses after the select list, so the name rides out as a
24548        // marker in partition_by; parse_bare_select substitutes the
24549        // definition once the clause is known.
24550        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24551            let name = w.clone();
24552            self.advance();
24553            return Ok((
24554                alloc::vec![Expr::Column(crate::ast::ColumnName {
24555                    qualifier: Some("__named_window__".to_string()),
24556                    name,
24557                })],
24558                Vec::new(),
24559                None,
24560            ));
24561        }
24562        if !matches!(self.peek(), Token::LParen) {
24563            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24564        }
24565        self.advance();
24566        let mut partition_by = Vec::new();
24567        let mut order_by = Vec::new();
24568        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24569        // window, refined in place. PG's rules (probed against 18.4) differ
24570        // from the bare `OVER w1` form, so the reference rides out under its
24571        // own marker and `substitute_named_windows` applies them. The base
24572        // name is any leading identifier that isn't a window-spec keyword.
24573        let base_window = match self.peek() {
24574            Token::Ident(s) | Token::QuotedIdent(s)
24575                if !s.eq_ignore_ascii_case("partition")
24576                    && !s.eq_ignore_ascii_case("rows")
24577                    && !s.eq_ignore_ascii_case("range")
24578                    && !s.eq_ignore_ascii_case("groups") =>
24579            {
24580                let n = s.clone();
24581                self.advance();
24582                Some(n)
24583            }
24584            _ => None,
24585        };
24586        // PARTITION BY ?
24587        // v7.37.6-B promoted PARTITION to a reserved keyword
24588        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
24589        // `Token::Ident("partition")`. Accept both so older sources
24590        // and the new lexer surface land on the same path.
24591        let is_partition_kw = match self.peek() {
24592            Token::Partition => true,
24593            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
24594            _ => false,
24595        };
24596        if is_partition_kw {
24597            self.advance();
24598            if !self.peek_is_by() {
24599                return Err(self.err(format!(
24600                    "expected BY after PARTITION, got {:?}",
24601                    self.peek()
24602                )));
24603            }
24604            self.advance();
24605            loop {
24606                partition_by.push(self.parse_expr(0)?);
24607                if matches!(self.peek(), Token::Comma) {
24608                    self.advance();
24609                    continue;
24610                }
24611                break;
24612            }
24613        }
24614        // ORDER BY ?
24615        if matches!(self.peek(), Token::Order) {
24616            self.advance();
24617            if !self.peek_is_by() {
24618                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24619            }
24620            self.advance();
24621            loop {
24622                let e = self.parse_expr(0)?;
24623                let desc = if matches!(self.peek(), Token::Desc) {
24624                    self.advance();
24625                    true
24626                } else if matches!(self.peek(), Token::Asc) {
24627                    self.advance();
24628                    false
24629                } else {
24630                    false
24631                };
24632                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
24633                let nulls_first = self.parse_optional_nulls_placement()?;
24634                order_by.push((e, desc, nulls_first));
24635                if matches!(self.peek(), Token::Comma) {
24636                    self.advance();
24637                    continue;
24638                }
24639                break;
24640            }
24641        }
24642        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
24643        // Both keywords come through the lexer as identifiers; match
24644        // case-insensitively.
24645        let mut frame: Option<WindowFrame> = None;
24646        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
24647            let kind = if s.eq_ignore_ascii_case("rows") {
24648                Some(FrameKind::Rows)
24649            } else if s.eq_ignore_ascii_case("range") {
24650                Some(FrameKind::Range)
24651            } else if s.eq_ignore_ascii_case("groups") {
24652                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
24653                Some(FrameKind::Groups)
24654            } else {
24655                None
24656            };
24657            if let Some(kind) = kind {
24658                self.advance();
24659                frame = Some(self.parse_frame_tail(kind)?);
24660            }
24661        }
24662        if !matches!(self.peek(), Token::RParen) {
24663            return Err(self.err(format!(
24664                "expected ')' to close OVER clause, got {:?}",
24665                self.peek()
24666            )));
24667        }
24668        self.advance();
24669        if let Some(base) = base_window {
24670            // A copy may refine but never override the base's partitioning
24671            // (PG rejects it outright, before looking the name up).
24672            if !partition_by.is_empty() {
24673                return Err(self.err(alloc::format!(
24674                    "cannot override PARTITION BY clause of window \"{base}\""
24675                )));
24676            }
24677            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
24678                qualifier: Some("__named_window_ref__".to_string()),
24679                name: base,
24680            })];
24681        }
24682        Ok((partition_by, order_by, frame))
24683    }
24684
24685    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
24686    /// or `RANGE` keyword was just consumed. Accepts both
24687    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
24688    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
24689    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
24690    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
24691        let (start, end) = if matches!(self.peek(), Token::Between) {
24692            self.advance();
24693            let start = self.parse_frame_bound()?;
24694            if !matches!(self.peek(), Token::And) {
24695                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
24696            }
24697            self.advance();
24698            let end = self.parse_frame_bound()?;
24699            (start, Some(end))
24700        } else {
24701            (self.parse_frame_bound()?, None)
24702        };
24703        let exclude = self.parse_frame_exclusion()?;
24704        Ok(WindowFrame {
24705            kind,
24706            start,
24707            end,
24708            exclude,
24709        })
24710    }
24711
24712    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
24713    /// after a frame spec. NO OTHERS is the default no-op.
24714    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
24715        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
24716            return Ok(FrameExclusion::NoOthers);
24717        }
24718        self.advance(); // EXCLUDE
24719        match self.peek() {
24720            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
24721                self.advance();
24722                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
24723                    return Err(self.err(format!(
24724                        "expected ROW after EXCLUDE CURRENT, got {:?}",
24725                        self.peek()
24726                    )));
24727                }
24728                self.advance();
24729                Ok(FrameExclusion::CurrentRow)
24730            }
24731            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
24732            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
24733            // Without this arm it fell to the catch-all, whose message
24734            // self-contradictingly listed GROUP as expected.
24735            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
24736                self.advance();
24737                Ok(FrameExclusion::Group)
24738            }
24739            Token::Group => {
24740                self.advance();
24741                Ok(FrameExclusion::Group)
24742            }
24743            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
24744                self.advance();
24745                Ok(FrameExclusion::Ties)
24746            }
24747            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
24748                self.advance();
24749                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
24750                    return Err(self.err(format!(
24751                        "expected OTHERS after EXCLUDE NO, got {:?}",
24752                        self.peek()
24753                    )));
24754                }
24755                self.advance();
24756                Ok(FrameExclusion::NoOthers)
24757            }
24758            other => Err(self.err(format!(
24759                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
24760            ))),
24761        }
24762    }
24763
24764    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
24765    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
24766    /// `UNBOUNDED FOLLOWING`.
24767    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
24768        // Interval-typed offset for a value-based RANGE frame over a
24769        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
24770        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
24771        // PRECEDING`.
24772        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
24773            let dir = self.expect_ident_like()?;
24774            return if dir.eq_ignore_ascii_case("preceding") {
24775                Ok(FrameBound::IntervalPreceding {
24776                    months,
24777                    days,
24778                    micros,
24779                })
24780            } else if dir.eq_ignore_ascii_case("following") {
24781                Ok(FrameBound::IntervalFollowing {
24782                    months,
24783                    days,
24784                    micros,
24785                })
24786            } else {
24787                Err(self.err(format!(
24788                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
24789                )))
24790            };
24791        }
24792        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
24793        if let Token::Integer(n) = *self.peek() {
24794            self.advance();
24795            let n: u64 = u64::try_from(n).map_err(|_| {
24796                self.err(format!(
24797                    "invalid frame offset {n} — expected non-negative integer"
24798                ))
24799            })?;
24800            let dir = self.expect_ident_like()?;
24801            return if dir.eq_ignore_ascii_case("preceding") {
24802                Ok(FrameBound::OffsetPreceding(n))
24803            } else if dir.eq_ignore_ascii_case("following") {
24804                Ok(FrameBound::OffsetFollowing(n))
24805            } else {
24806                Err(self.err(format!(
24807                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
24808                )))
24809            };
24810        }
24811        let first = self.expect_ident_like()?;
24812        if first.eq_ignore_ascii_case("unbounded") {
24813            let dir = self.expect_ident_like()?;
24814            return if dir.eq_ignore_ascii_case("preceding") {
24815                Ok(FrameBound::UnboundedPreceding)
24816            } else if dir.eq_ignore_ascii_case("following") {
24817                Ok(FrameBound::UnboundedFollowing)
24818            } else {
24819                Err(self.err(format!(
24820                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
24821                )))
24822            };
24823        }
24824        if first.eq_ignore_ascii_case("current") {
24825            let row = self.expect_ident_like()?;
24826            if !row.eq_ignore_ascii_case("row") {
24827                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
24828            }
24829            return Ok(FrameBound::CurrentRow);
24830        }
24831        Err(self.err(format!(
24832            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
24833        )))
24834    }
24835
24836    /// Detect and consume a leading interval offset in a frame bound —
24837    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
24838    /// `(months, days, micros)`. Leaves the cursor on the trailing
24839    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
24840    /// when the next tokens are not an interval offset.
24841    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
24842        // Shape A — `INTERVAL '1 day'`.
24843        if matches!(self.peek(), Token::Interval) {
24844            self.advance(); // INTERVAL
24845            let atom = self.parse_interval_atom()?;
24846            if let Expr::Literal(Literal::Interval {
24847                months,
24848                days,
24849                micros,
24850                ..
24851            }) = atom
24852            {
24853                return Ok(Some((months, days, micros)));
24854            }
24855            return Err(self.err("expected an interval literal in frame offset".to_string()));
24856        }
24857        // Shape B — `'1 day'::interval`. Look ahead for the exact
24858        // string / `::` / interval-target triple before committing.
24859        if let Token::String(text) = self.peek() {
24860            let target_is_interval = match self.tokens.get(self.pos + 2) {
24861                Some(Token::Interval) => true,
24862                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
24863                _ => false,
24864            };
24865            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
24866                && target_is_interval;
24867            if is_cast {
24868                let text = text.clone();
24869                self.advance(); // string
24870                self.advance(); // ::
24871                self.advance(); // interval
24872                let parts = parse_interval_text(&text).ok_or_else(|| {
24873                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
24874                })?;
24875                return Ok(Some(parts));
24876            }
24877        }
24878        Ok(None)
24879    }
24880
24881    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
24882        if matches!(self.peek(), Token::Dot) {
24883            self.advance();
24884            let name = self.expect_ident_like()?;
24885            // v7.14.0 — schema-qualified function call
24886            // `<schema>.<fn>(args)`. PG dumps emit
24887            // `pg_catalog.set_config(...)` in the preamble. SPG
24888            // is single-namespace: drop the schema prefix and
24889            // route the dispatch on the bare function name.
24890            if matches!(self.peek(), Token::LParen) {
24891                return self.finish_ident_atom(name);
24892            }
24893            return Ok(Expr::Column(ColumnName {
24894                qualifier: Some(first),
24895                name,
24896            }));
24897        }
24898        if matches!(self.peek(), Token::LParen) {
24899            self.advance();
24900            // `COUNT(*)` — special-cased here because `*` isn't a normal
24901            // expression token. Lower-case match on `first` since the lexer
24902            // folds identifiers.
24903            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
24904                self.advance();
24905                if !matches!(self.peek(), Token::RParen) {
24906                    return Err(self.err(format!(
24907                        "expected ')' after COUNT(*), got {:?}",
24908                        self.peek()
24909                    )));
24910                }
24911                self.advance();
24912                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
24913                let filter = self.parse_filter_clause()?;
24914                // v4.12: COUNT(*) OVER (...) — same window tail.
24915                let null_treatment = self.parse_null_treatment_modifier();
24916                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24917                    && s.eq_ignore_ascii_case("over")
24918                {
24919                    self.advance();
24920                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
24921                    return Ok(Expr::WindowFunction {
24922                        name: "count_star".into(),
24923                        args: Vec::new(),
24924                        partition_by,
24925                        order_by,
24926                        frame,
24927                        null_treatment,
24928                        filter,
24929                    });
24930                }
24931                if let Some(filter) = filter {
24932                    return Ok(Expr::AggregateOrdered {
24933                        call: Box::new(Expr::FunctionCall {
24934                            name: "count_star".into(),
24935                            args: Vec::new(),
24936                        }),
24937                        order_by: Vec::new(),
24938                        distinct: false,
24939                        filter: Some(filter),
24940                    });
24941                }
24942                return Ok(Expr::FunctionCall {
24943                    name: "count_star".into(),
24944                    args: Vec::new(),
24945                });
24946            }
24947            // Function call. PG-style: zero-or-more comma-separated args.
24948            let mut args = Vec::new();
24949            // v7.38 (read01, T14) — named-argument notation `argname => value`.
24950            // Names are collected in lock-step with `args` and resolved to
24951            // positional order after the loop (the AST stays positional).
24952            let mut arg_names: Vec<Option<String>> = Vec::new();
24953            let mut agg_order_by: Vec<OrderBy> = Vec::new();
24954            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
24955            // seen, so the value arguments before it can be folded.
24956            let mut saw_separator = false;
24957            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
24958            // v7.32 (round-29) — accept the dual `ALL` quantifier too
24959            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
24960            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
24961                self.advance();
24962                true
24963            } else if matches!(self.peek(), Token::All) {
24964                self.advance();
24965                false
24966            } else {
24967                false
24968            };
24969            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
24970            // TIMESTAMPDIFF take a bare unit keyword as the first
24971            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
24972            // bare type keyword (DATE / TIME / DATETIME); lower them
24973            // onto string literals so the evaluator sees plain text.
24974            if ((first.eq_ignore_ascii_case("timestampadd")
24975                || first.eq_ignore_ascii_case("timestampdiff"))
24976                && matches!(self.peek(), Token::Ident(u) if matches!(
24977                    u.to_ascii_lowercase().as_str(),
24978                    "microsecond" | "second" | "minute" | "hour" | "day"
24979                        | "week" | "month" | "quarter" | "year"
24980                )))
24981                || (first.eq_ignore_ascii_case("get_format")
24982                    && matches!(self.peek(), Token::Ident(u) if matches!(
24983                        u.to_ascii_lowercase().as_str(),
24984                        "date" | "time" | "datetime" | "timestamp"
24985                    )))
24986            {
24987                if let Token::Ident(u) = self.peek() {
24988                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
24989                }
24990                self.advance();
24991                if matches!(self.peek(), Token::Comma) {
24992                    self.advance();
24993                }
24994            }
24995            // `ROW(a, b, …)` keyword constructor. Followed by a
24996            // comparison operator or [NOT] IN it joins the paren
24997            // row-constructor machinery (fieldwise parse-time
24998            // expansion); bare, it stays a `row` call the evaluator
24999            // renders as PG record text.
25000            if first.eq_ignore_ascii_case("row") {
25001                let mut row_items = Vec::new();
25002                if !matches!(self.peek(), Token::RParen) {
25003                    loop {
25004                        row_items.push(self.parse_expr(0)?);
25005                        match self.peek() {
25006                            Token::Comma => {
25007                                self.advance();
25008                            }
25009                            Token::RParen => break,
25010                            other => {
25011                                return Err(self.err(format!(
25012                                    "expected ',' or ')' in ROW(...), got {other:?}"
25013                                )));
25014                            }
25015                        }
25016                    }
25017                }
25018                self.advance(); // ')'
25019                let comparison_follows = matches!(
25020                    self.peek(),
25021                    Token::Eq
25022                        | Token::NotEq
25023                        | Token::Lt
25024                        | Token::LtEq
25025                        | Token::Gt
25026                        | Token::GtEq
25027                        | Token::In
25028                ) || (matches!(self.peek(), Token::Not)
25029                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25030                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25031                if comparison_follows && !row_items.is_empty() {
25032                    return self.parse_row_comparison_tail(row_items);
25033                }
25034                return Ok(Expr::FunctionCall {
25035                    name: String::from("row"),
25036                    args: row_items,
25037                });
25038            }
25039            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25040            // the parse-mode keyword introduces the source text. SPG
25041            // carries XML as text, so both modes lower to __xmlparse(expr)
25042            // which validates well-formedness and returns Value::Xml.
25043            if first.eq_ignore_ascii_case("xmlparse")
25044                && matches!(self.peek(), Token::Ident(kw)
25045                    if kw.eq_ignore_ascii_case("document")
25046                        || kw.eq_ignore_ascii_case("content"))
25047            {
25048                let mode = match self.advance() {
25049                    Token::Ident(kw) => kw.to_ascii_lowercase(),
25050                    _ => unreachable!("peeked an ident"),
25051                };
25052                let src = self.parse_expr(0)?;
25053                if !matches!(self.peek(), Token::RParen) {
25054                    return Err(self.err(format!(
25055                        "expected ')' to close XMLPARSE, got {:?}",
25056                        self.peek()
25057                    )));
25058                }
25059                self.advance();
25060                return Ok(Expr::FunctionCall {
25061                    name: String::from("__xmlparse"),
25062                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25063                });
25064            }
25065            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25066            // keyword introduces the element name (a bare or quoted
25067            // identifier), then optional content expressions. Lower to a
25068            // plain `xmlelement(name_text, content …)` call.
25069            if first.eq_ignore_ascii_case("xmlelement")
25070                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25071            {
25072                self.advance(); // consume NAME
25073                let elem_name = match self.peek().clone() {
25074                    Token::Ident(n) | Token::QuotedIdent(n) => {
25075                        self.advance();
25076                        n
25077                    }
25078                    other => {
25079                        return Err(self.err(format!(
25080                            "expected element name after XMLELEMENT NAME, got {other:?}"
25081                        )));
25082                    }
25083                };
25084                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25085                while matches!(self.peek(), Token::Comma) {
25086                    self.advance();
25087                    args.push(self.parse_expr(0)?);
25088                }
25089                if !matches!(self.peek(), Token::RParen) {
25090                    return Err(self.err(format!(
25091                        "expected ')' to close XMLELEMENT, got {:?}",
25092                        self.peek()
25093                    )));
25094                }
25095                self.advance();
25096                return Ok(Expr::FunctionCall {
25097                    name: String::from("xmlelement"),
25098                    args,
25099                });
25100            }
25101            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25102            // becomes a `<name>value</name>` element; a bare column infers its
25103            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25104            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25105                let mut args: Vec<Expr> = Vec::new();
25106                loop {
25107                    let val = self.parse_expr(0)?;
25108                    let name = if matches!(self.peek(), Token::As) {
25109                        self.advance();
25110                        match self.peek().clone() {
25111                            Token::Ident(n) | Token::QuotedIdent(n) => {
25112                                self.advance();
25113                                n
25114                            }
25115                            other => {
25116                                return Err(self.err(format!(
25117                                    "expected name after AS in XMLFOREST, got {other:?}"
25118                                )));
25119                            }
25120                        }
25121                    } else if let Expr::Column(c) = &val {
25122                        c.name.clone()
25123                    } else {
25124                        return Err(
25125                            self.err("XMLFOREST element without a column name needs AS".into())
25126                        );
25127                    };
25128                    args.push(Expr::Literal(Literal::String(name)));
25129                    args.push(val);
25130                    if matches!(self.peek(), Token::Comma) {
25131                        self.advance();
25132                    } else {
25133                        break;
25134                    }
25135                }
25136                if !matches!(self.peek(), Token::RParen) {
25137                    return Err(self.err(format!(
25138                        "expected ')' to close XMLFOREST, got {:?}",
25139                        self.peek()
25140                    )));
25141                }
25142                self.advance();
25143                return Ok(Expr::FunctionCall {
25144                    name: String::from("xmlforest"),
25145                    args,
25146                });
25147            }
25148            // SQL-standard `POSITION(sub IN str)` — lowers onto
25149            // strpos(str, sub). IN is the argument separator here,
25150            // so the needle parses with the IN-tail suppressed.
25151            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25152                let saved = self.suppress_in_tail;
25153                self.suppress_in_tail = true;
25154                let needle = self.parse_expr(0);
25155                self.suppress_in_tail = saved;
25156                let needle = needle?;
25157                if matches!(self.peek(), Token::In) {
25158                    self.advance();
25159                    let haystack = self.parse_expr(0)?;
25160                    if !matches!(self.peek(), Token::RParen) {
25161                        return Err(self.err(format!(
25162                            "expected ')' to close POSITION, got {:?}",
25163                            self.peek()
25164                        )));
25165                    }
25166                    self.advance();
25167                    return Ok(Expr::FunctionCall {
25168                        name: String::from("strpos"),
25169                        args: alloc::vec![haystack, needle],
25170                    });
25171                }
25172                // position(sub, str) comma form (incl. bytea) —
25173                // hand the parsed first arg to the generic list.
25174                args.push(needle);
25175                if matches!(self.peek(), Token::Comma) {
25176                    self.advance();
25177                }
25178            }
25179            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25180            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25181            // plain comma forms TRIM(str) / TRIM(str, chars) keep
25182            // riding the generic argument list below.
25183            if first.eq_ignore_ascii_case("trim") {
25184                let mode = match self.peek() {
25185                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25186                        self.advance();
25187                        Some("btrim")
25188                    }
25189                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25190                        self.advance();
25191                        Some("ltrim")
25192                    }
25193                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25194                        self.advance();
25195                        Some("rtrim")
25196                    }
25197                    _ => None,
25198                };
25199                if mode.is_some() || matches!(self.peek(), Token::From) {
25200                    // TRIM([mode] FROM str) — no strip-chars.
25201                    let chars = if matches!(self.peek(), Token::From) {
25202                        None
25203                    } else {
25204                        Some(self.parse_expr(0)?)
25205                    };
25206                    if !matches!(self.peek(), Token::From) {
25207                        return Err(self.err(format!(
25208                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25209                            self.peek()
25210                        )));
25211                    }
25212                    self.advance();
25213                    let target = self.parse_expr(0)?;
25214                    if !matches!(self.peek(), Token::RParen) {
25215                        return Err(
25216                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25217                        );
25218                    }
25219                    self.advance();
25220                    let mut trim_args = alloc::vec![target];
25221                    if let Some(c) = chars {
25222                        trim_args.push(c);
25223                    }
25224                    return Ok(Expr::FunctionCall {
25225                        name: String::from(mode.unwrap_or("btrim")),
25226                        args: trim_args,
25227                    });
25228                }
25229            }
25230            if !matches!(self.peek(), Token::RParen) {
25231                loop {
25232                    // v7.38 (read01, T14) — `argname => value` names this arg.
25233                    // v7.39 (read01 round 77) — `argname := value` is the same
25234                    // thing, and it is the spelling PG's own docs lead with. It
25235                    // was simply never lexed here, so every `f(x := 1)` died in
25236                    // the parser regardless of what `f` was.
25237                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25238                        (
25239                            Token::Ident(n) | Token::QuotedIdent(n),
25240                            Some(Token::FatArrow | Token::ColonEq),
25241                        ) => {
25242                            let name = n.clone();
25243                            self.advance(); // name
25244                            self.advance(); // => / :=
25245                            Some(name)
25246                        }
25247                        _ => None,
25248                    };
25249                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25250                    // array's elements into a variadic call's trailing args
25251                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25252                    // reserved, so it arrives as a bare ident before the arg.
25253                    let is_variadic = this_name.is_none()
25254                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25255                    if is_variadic {
25256                        self.advance();
25257                    }
25258                    let arg = self.parse_expr(0)?;
25259                    args.push(match &this_name {
25260                        // The callee's parameter names decide the slot, and a
25261                        // user function's live in the catalog. Carry the name
25262                        // to eval rather than guessing here.
25263                        Some(n) => Expr::NamedArg {
25264                            name: n.clone(),
25265                            expr: Box::new(arg),
25266                        },
25267                        None if is_variadic => Expr::Variadic(Box::new(arg)),
25268                        None => arg,
25269                    });
25270                    arg_names.push(this_name);
25271                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
25272                    // The `::` cast already worked; this lowers the
25273                    // function form onto the same Expr::Cast node.
25274                    if first.eq_ignore_ascii_case("cast")
25275                        && args.len() == 1
25276                        && matches!(self.peek(), Token::As)
25277                    {
25278                        self.advance();
25279                        let target = self.parse_cast_target()?;
25280                        if !matches!(self.peek(), Token::RParen) {
25281                            return Err(self.err(format!(
25282                                "expected ')' to close CAST, got {:?}",
25283                                self.peek()
25284                            )));
25285                        }
25286                        self.advance();
25287                        return Ok(Expr::Cast {
25288                            expr: Box::new(args.pop().expect("one arg")),
25289                            target,
25290                        });
25291                    }
25292                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25293                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25294                    // keywords; SPG's lexer makes them plain idents (so they'd be
25295                    // read as column refs). Lower the keyword to the string form
25296                    // the evaluator already accepts.
25297                    if first.eq_ignore_ascii_case("normalize")
25298                        && args.len() == 1
25299                        && matches!(self.peek(), Token::Comma)
25300                    {
25301                        let form = match self.tokens.get(self.pos + 1) {
25302                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25303                                let up = f.to_ascii_uppercase();
25304                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25305                            }
25306                            _ => None,
25307                        };
25308                        if let Some(up) = form {
25309                            self.advance(); // comma
25310                            self.advance(); // form keyword
25311                            args.push(Expr::Literal(Literal::String(up)));
25312                        }
25313                    }
25314                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25315                    // form. Desugars to the comma-list shape evaluator already
25316                    // handles. Triggered after the first arg when the function
25317                    // name is substring / substr and the next token is FROM
25318                    // (a reserved keyword in PG; SPG also reserves it).
25319                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25320                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25321                    // internal __substring_similar(str, pat, esc) call.
25322                    if (first.eq_ignore_ascii_case("substring")
25323                        || first.eq_ignore_ascii_case("substr"))
25324                        && args.len() == 1
25325                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25326                    {
25327                        self.advance(); // SIMILAR
25328                        let pattern = self.parse_expr(0)?;
25329                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25330                        {
25331                            return Err(self.err(format!(
25332                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25333                                self.peek()
25334                            )));
25335                        }
25336                        self.advance(); // ESCAPE
25337                        let esc = self.parse_expr(0)?;
25338                        if !matches!(self.peek(), Token::RParen) {
25339                            return Err(self.err(format!(
25340                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
25341                                self.peek()
25342                            )));
25343                        }
25344                        self.advance();
25345                        args.push(pattern);
25346                        args.push(esc);
25347                        return Ok(Expr::FunctionCall {
25348                            name: "__substring_similar".to_string(),
25349                            args,
25350                        });
25351                    }
25352                    if (first.eq_ignore_ascii_case("substring")
25353                        || first.eq_ignore_ascii_case("substr"))
25354                        && args.len() == 1
25355                        && matches!(self.peek(), Token::From | Token::For)
25356                    {
25357                        // `substring(str FROM pos [FOR len])`, or the FOR-only
25358                        // `substring(str FOR len)` which PG treats as FROM 1.
25359                        if matches!(self.peek(), Token::From) {
25360                            self.advance();
25361                            let start = self.parse_expr(0)?;
25362                            args.push(start);
25363                        } else {
25364                            args.push(Expr::Literal(Literal::Integer(1)));
25365                        }
25366                        if matches!(self.peek(), Token::For) {
25367                            self.advance();
25368                            let length = self.parse_expr(0)?;
25369                            args.push(length);
25370                        }
25371                        if !matches!(self.peek(), Token::RParen) {
25372                            return Err(self.err(format!(
25373                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25374                                self.peek()
25375                            )));
25376                        }
25377                        self.advance();
25378                        return Ok(Expr::FunctionCall {
25379                            name: first.to_ascii_lowercase(),
25380                            args,
25381                        });
25382                    }
25383                    // PG `overlay(str PLACING repl FROM n [FOR len])`
25384                    // syntactic form. Desugars to the `overlay(str,
25385                    // repl, n[, len])` comma-list shape the evaluator
25386                    // already implements. `PLACING` is not a reserved
25387                    // token in SPG, so it arrives as a bare Ident.
25388                    if first.eq_ignore_ascii_case("overlay")
25389                        && args.len() == 1
25390                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25391                    {
25392                        self.advance(); // consume PLACING
25393                        args.push(self.parse_expr(0)?); // replacement
25394                        if !matches!(self.peek(), Token::From) {
25395                            return Err(self.err(format!(
25396                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25397                                self.peek()
25398                            )));
25399                        }
25400                        self.advance();
25401                        args.push(self.parse_expr(0)?); // start position
25402                        if matches!(self.peek(), Token::For) {
25403                            self.advance();
25404                            args.push(self.parse_expr(0)?); // length
25405                        }
25406                        if !matches!(self.peek(), Token::RParen) {
25407                            return Err(self.err(format!(
25408                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25409                                self.peek()
25410                            )));
25411                        }
25412                        self.advance();
25413                        return Ok(Expr::FunctionCall {
25414                            name: String::from("overlay"),
25415                            args,
25416                        });
25417                    }
25418                    // `TRIM(chars FROM str)` — the keyword-less
25419                    // spelling lands here after the chars parse
25420                    // (the keyword forms return earlier).
25421                    if first.eq_ignore_ascii_case("trim")
25422                        && args.len() == 1
25423                        && matches!(self.peek(), Token::From)
25424                    {
25425                        self.advance();
25426                        let target = self.parse_expr(0)?;
25427                        if !matches!(self.peek(), Token::RParen) {
25428                            return Err(self.err(format!(
25429                                "expected ')' to close TRIM(chars FROM str), got {:?}",
25430                                self.peek()
25431                            )));
25432                        }
25433                        self.advance();
25434                        let chars = args.pop().expect("one arg");
25435                        return Ok(Expr::FunctionCall {
25436                            name: String::from("btrim"),
25437                            args: alloc::vec![target, chars],
25438                        });
25439                    }
25440                    // v7.24 (round-16 A) — aggregate-internal
25441                    // ordering: `array_agg(x ORDER BY y DESC NULLS
25442                    // LAST)`. Keys close the argument list.
25443                    if matches!(self.peek(), Token::Order) {
25444                        self.advance();
25445                        if !self.peek_is_by() {
25446                            return Err(self.err(format!(
25447                                "expected BY after ORDER in aggregate args, got {:?}",
25448                                self.peek()
25449                            )));
25450                        }
25451                        self.advance();
25452                        loop {
25453                            // v7.39 (round 691) — save/restore, the discipline this parser
25454                            // already uses around `pending_sample_preds`, so a subquery inside
25455                            // a key neither inherits nor leaks the channel.
25456                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25457                            let saved_coll = self.order_key_collation.take();
25458                            let parsed = self.parse_expr(0);
25459                            self.in_order_by_key = saved_flag;
25460                            let collation =
25461                                core::mem::replace(&mut self.order_key_collation, saved_coll);
25462                            let expr = parsed?;
25463                            let desc = if matches!(self.peek(), Token::Desc) {
25464                                self.advance();
25465                                true
25466                            } else if matches!(self.peek(), Token::Asc) {
25467                                self.advance();
25468                                false
25469                            } else {
25470                                false
25471                            };
25472                            let nulls_first = self.parse_optional_nulls_placement()?;
25473                            agg_order_by.push(OrderBy {
25474                                expr,
25475                                desc,
25476                                nulls_first,
25477                                collation,
25478                            });
25479                            if matches!(self.peek(), Token::Comma) {
25480                                self.advance();
25481                            } else {
25482                                break;
25483                            }
25484                        }
25485                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25486                        // follow the ORDER BY inside GROUP_CONCAT.
25487                        if self.consume_group_concat_separator(&mut args)? {
25488                            saw_separator = true;
25489                        }
25490                        if !matches!(self.peek(), Token::RParen) {
25491                            return Err(self.err(format!(
25492                                "expected ')' after aggregate ORDER BY, got {:?}",
25493                                self.peek()
25494                            )));
25495                        }
25496                        break;
25497                    }
25498                    // v7.39 (round 354, M12) — …or directly after the
25499                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25500                    // own spelling of what PG passes as string_agg's second
25501                    // argument; it was a parse error, so every MySQL query
25502                    // that names its own separator failed outright.
25503                    if self.consume_group_concat_separator(&mut args)? {
25504                        saw_separator = true;
25505                        break;
25506                    }
25507                    match self.peek() {
25508                        Token::Comma => {
25509                            self.advance();
25510                        }
25511                        Token::RParen => break,
25512                        other => {
25513                            return Err(self.err(format!(
25514                                "expected ',' or ')' in function args, got {other:?}"
25515                            )));
25516                        }
25517                    }
25518                }
25519            }
25520            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25521            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25522            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25523            // meaning a separator — that is what the explicit SEPARATOR
25524            // tail is for. Fold them into one `concat(...)` so the
25525            // aggregate keeps its single value argument.
25526            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25527                let values = args.len() - usize::from(saw_separator);
25528                if values > 1 {
25529                    let sep_arg = if saw_separator { args.pop() } else { None };
25530                    let folded = Expr::FunctionCall {
25531                        name: "concat".to_string(),
25532                        args: core::mem::take(&mut args),
25533                    };
25534                    args.push(folded);
25535                    if let Some(sep) = sep_arg {
25536                        args.push(sep);
25537                    }
25538                }
25539            }
25540            self.advance(); // consume ')'
25541            // v7.39 (read01 round 77) — named arguments are NOT reordered here
25542            // any more. The parser has no catalog, so it could only ever resolve
25543            // the handful of `make_*` builtins whose parameter names were baked
25544            // into a table right here — every user function got
25545            // "does not support named arguments", though the catalog has been
25546            // storing its parameter names all along. Reordering happens in eval,
25547            // in one place, for builtins and user functions alike.
25548            // v7.32 (round-29) — ordered-set aggregate tail
25549            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25550            // (percentile_cont / percentile_disc / mode). The sort spec
25551            // lands in the same `order_by` slot a decorated aggregate
25552            // uses; the executor dispatches on the function name. WITHIN
25553            // GROUP and an intra-argument ORDER BY are mutually
25554            // exclusive (PG rejects both).
25555            let within_group_order = self.parse_within_group_clause()?;
25556            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25557                return Err(self.err(
25558                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25559                        .into(),
25560                ));
25561            }
25562            let within_group_seen = !within_group_order.is_empty();
25563            let agg_order_by = if within_group_order.is_empty() {
25564                agg_order_by
25565            } else {
25566                within_group_order
25567            };
25568            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25569            let filter = self.parse_filter_clause()?;
25570            // v4.12: window-function tail — `name(args) OVER (...)`.
25571            // Promotes the just-parsed FunctionCall into a
25572            // WindowFunction node carrying partition + order.
25573            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25574            // / `RESPECT NULLS OVER (...)` between the closing paren
25575            // and `OVER`.
25576            let null_treatment = self.parse_null_treatment_modifier();
25577            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25578                && s.eq_ignore_ascii_case("over")
25579            {
25580                self.advance();
25581                // v7.39 (round 230) — PG implements neither modifier for a
25582                // windowed call and says so (0A000). Both used to be parsed
25583                // and then silently dropped here, so `count(DISTINCT v)
25584                // OVER (…)` quietly answered the non-distinct count.
25585                if agg_distinct {
25586                    return Err(
25587                        self.err("DISTINCT is not implemented for window functions".to_string())
25588                    );
25589                }
25590                if !agg_order_by.is_empty() {
25591                    // PG separates the two shapes that land here: a
25592                    // WITHIN GROUP call is an ordered-set aggregate and gets
25593                    // its own message naming the aggregate; a plain
25594                    // `agg(x ORDER BY y)` gets the generic one.
25595                    let msg = if within_group_seen {
25596                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
25597                    } else {
25598                        "aggregate ORDER BY is not implemented for window functions".to_string()
25599                    };
25600                    return Err(self.err(msg));
25601                }
25602                let (partition_by, order_by, frame) = self.parse_over_clause()?;
25603                return Ok(Expr::WindowFunction {
25604                    name: first,
25605                    args,
25606                    partition_by,
25607                    order_by,
25608                    frame,
25609                    null_treatment,
25610                    filter,
25611                });
25612            }
25613            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
25614                return Ok(Expr::AggregateOrdered {
25615                    call: Box::new(Expr::FunctionCall { name: first, args }),
25616                    order_by: agg_order_by,
25617                    distinct: agg_distinct,
25618                    filter,
25619                });
25620            }
25621            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
25622            // over TIMESTAMPTZ and has no timestamp overload, so a
25623            // timestamp argument is coerced on the way in and the answer
25624            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
25625            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
25626            // zone`. SPG answered `timestamp without time zone`, dropping
25627            // the offset from every rendering.
25628            //
25629            // Writing the coercion PG performs makes the existing
25630            // argument-driven typing (the one `date_trunc` uses) reach the
25631            // right answer, rather than teaching the type layer a second
25632            // rule. MySQL's DATE_ADD is a different function that returns
25633            // DATE or DATETIME, so this is PG-dialect only.
25634            //
25635            // Out-of-line because this sits on the RECURSIVE descent
25636            // frame: an inline block with locals here costs every nesting
25637            // level, and the suite's deep-nesting sentinel overflowed the
25638            // 512 KiB parser stack the moment one was added (round 430's
25639            // lesson, in the same shape).
25640            if !self.mysql_dialect {
25641                lift_date_add_arg_to_timestamptz(&first, &mut args);
25642            }
25643            return Ok(Expr::FunctionCall { name: first, args });
25644        }
25645        // v7.9.20 — SQL-standard parenless keyword expressions
25646        // (PG treats these as functions called without parens).
25647        // Resolve to a synthetic FunctionCall so the engine's
25648        // eval path reuses the existing function-call routing.
25649        // mailrs G3.
25650        let lc = first.to_ascii_lowercase();
25651        if matches!(
25652            lc.as_str(),
25653            "current_date"
25654                | "current_time"
25655                | "current_timestamp"
25656                | "localtimestamp"
25657                | "localtime"
25658                // v7.37.17 (17.6 siblings) — session-identity SQL-
25659                // standard parenless keywords. current_user /
25660                // session_user / user were already caught by the
25661                // pgwire canned-response shortcut but bare-select
25662                // in the embedded engine went through Expr::Column
25663                // and errored. Adding them here so the parser
25664                // resolves to a synthetic FunctionCall that reuses
25665                // the existing eval/functions.rs dispatch.
25666                | "current_user"
25667                | "session_user"
25668                | "current_role"
25669                | "current_catalog"
25670                | "current_schema"
25671                | "current_database"
25672                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
25673                | "system_user"
25674        ) {
25675            return Ok(Expr::FunctionCall {
25676                name: lc,
25677                args: Vec::new(),
25678            });
25679        }
25680        Ok(Expr::Column(ColumnName {
25681            qualifier: None,
25682            name: first,
25683        }))
25684    }
25685}
25686
25687/// v7.39 (round 522) — write the coercion PG's `date_add` /
25688/// `date_subtract` signature performs.
25689///
25690/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
25691/// timestamp argument is cast on the way in and the answer is
25692/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
25693/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
25694/// `timestamp without time zone`, dropping the offset from every
25695/// rendering of the result.
25696///
25697/// Writing the cast the signature implies lets the existing
25698/// argument-driven typing (the one `date_trunc` uses) reach the right
25699/// answer instead of teaching the type layer a second rule. MySQL's
25700/// DATE_ADD is a different function returning DATE or DATETIME, so the
25701/// caller applies this in PG dialect only.
25702///
25703/// A free function, and not a block at the call site, because the caller
25704/// is on the recursive-descent frame chain.
25705#[inline(never)]
25706fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
25707    if args.len() != 2
25708        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
25709    {
25710        return;
25711    }
25712    let base = args.remove(0);
25713    args.insert(
25714        0,
25715        Expr::Cast {
25716            expr: Box::new(base),
25717            target: CastTarget::Timestamptz,
25718        },
25719    );
25720}
25721
25722/// v6.8.2 — walk an expression tree and return the first column
25723/// reference's bare name. Used by `parse_create_index_stmt_after_create`
25724/// to derive `CreateIndexStatement.column` from an expression
25725/// key (so downstream planner code resolving a primary column
25726/// position keeps working with expression indexes). Returns
25727/// `None` when the expression has no column ref at all — caller
25728/// surfaces that as a parse error.
25729fn extract_first_column(expr: &Expr) -> Option<String> {
25730    match expr {
25731        Expr::Column(cn) => Some(cn.name.clone()),
25732        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
25733        Expr::Binary { lhs, rhs, .. } => {
25734            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
25735        }
25736        Expr::Unary { expr: e, .. } => extract_first_column(e),
25737        // v7.39 (read01 round 93) — a cast wraps its operand: a common
25738        // expression-index key is `lower(col::text)`, where the column
25739        // sits under the `::text` cast inside the function arg. Without
25740        // descending here the key was rejected as "references no column".
25741        Expr::Cast { expr: e, .. } => extract_first_column(e),
25742        _ => None,
25743    }
25744}
25745
25746fn maybe_not(expr: Expr, negated: bool) -> Expr {
25747    if negated {
25748        Expr::Unary {
25749            op: UnOp::Not,
25750            expr: Box::new(expr),
25751        }
25752    } else {
25753        expr
25754    }
25755}
25756
25757/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
25758/// things in the two dialects, and SPG read all three PG's way:
25759///
25760/// | token | PG (and SPG) | MySQL, measured |
25761/// |---|---|---|
25762/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
25763/// | `&&` | inet / array overlap | **AND** |
25764/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
25765///
25766/// `1 || 0` answering the string '10' on a MySQL session is a wrong
25767/// answer with no error, which is why they are routed here rather than
25768/// left to the shared table.
25769impl Parser {
25770    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
25771        if self.mysql_dialect {
25772            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
25773            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
25774            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
25775            if let Token::Ident(w) = tok
25776                && w.eq_ignore_ascii_case("div")
25777            {
25778                return Some((BinOp::IntDiv, 8));
25779            }
25780            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
25781            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
25782            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
25783            // there sits in operand position, not infix).
25784            if let Token::Ident(w) = tok
25785                && w.eq_ignore_ascii_case("mod")
25786            {
25787                return Some((BinOp::Mod, 8));
25788            }
25789            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
25790            // plain ident to the lexer. Its precedence sits between OR (1)
25791            // and AND (3) — hence rung 2, the slot freed by moving AND up.
25792            if let Token::Ident(w) = tok
25793                && w.eq_ignore_ascii_case("xor")
25794            {
25795                return Some((BinOp::LogicalXor, 2));
25796            }
25797            match tok {
25798                Token::Concat => return Some((BinOp::Or, 1)),
25799                // MySQL's `&&` is logical AND, sharing AND's rung (3).
25800                Token::InetOverlap => return Some((BinOp::And, 3)),
25801                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
25802                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
25803                _ => {}
25804            }
25805        }
25806        binop_from(tok)
25807    }
25808}
25809
25810// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
25811// (which sits strictly between OR and AND), every level from AND upward was
25812// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
25813// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
25814// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
25815// the *relative* order of every PG operator is unchanged by the shift.
25816fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
25817    let pair = match tok {
25818        Token::Or => (BinOp::Or, 1),
25819        Token::And => (BinOp::And, 3),
25820        Token::Eq => (BinOp::Eq, 5),
25821        Token::NotEq => (BinOp::NotEq, 5),
25822        Token::Lt => (BinOp::Lt, 5),
25823        Token::LtEq => (BinOp::LtEq, 5),
25824        Token::Gt => (BinOp::Gt, 5),
25825        Token::GtEq => (BinOp::GtEq, 5),
25826        // pgvector distance ops all sit on the same rung — tighter than
25827        // comparisons (5) so `col <-> v < threshold` parses correctly.
25828        Token::L2Distance => (BinOp::L2Distance, 6),
25829        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
25830        // comparison rung.
25831        Token::GeomParallel => (BinOp::GeomParallel, 5),
25832        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
25833        // comparison rung.
25834        Token::OverLeft => (BinOp::OverLeft, 5),
25835        Token::OverRight => (BinOp::OverRight, 5),
25836        Token::GeomPerp => (BinOp::GeomPerp, 5),
25837        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
25838        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
25839        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
25840        Token::InnerProduct => (BinOp::InnerProduct, 6),
25841        Token::CosineDistance => (BinOp::CosineDistance, 6),
25842        Token::Plus => (BinOp::Add, 7),
25843        Token::Minus => (BinOp::Sub, 7),
25844        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
25845        // binds every "other" operator (`||`, `|`, `&`, `#`, the
25846        // pgvector distances above) BETWEEN additive (7) and the
25847        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
25848        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
25849        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
25850        // ("matches PG conceptually" — the round-753 audit measured it
25851        // false; the old rung errored on `'a' || 1 + 1` with
25852        // `text + integer`). Same-level chains left-fold, as PG does.
25853        Token::Concat => (BinOp::Concat, 6),
25854        Token::Pipe => (BinOp::BitOr, 6),
25855        Token::Amp => (BinOp::BitAnd, 6),
25856        Token::Star => (BinOp::Mul, 8),
25857        Token::Slash => (BinOp::Div, 8),
25858        Token::Percent => (BinOp::Mod, 8),
25859        // v4.14: JSON path ops bind tighter than comparisons (5)
25860        // and additive (7) so `doc->'k' = 'v'` parses correctly.
25861        // Same rung as the multiplicative ops.
25862        Token::JsonGet => (BinOp::JsonGet, 8),
25863        Token::JsonGetText => (BinOp::JsonGetText, 8),
25864        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
25865        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
25866        Token::JsonContains => (BinOp::JsonContains, 8),
25867        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
25868        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
25869        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
25870        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
25871        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
25872        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
25873        // v7.12.2 — `@@` binds at the comparison rung (looser than
25874        // arithmetic, tighter than AND / OR). PG places `@@` at
25875        // the same precedence as `=` / `<`, so we follow.
25876        Token::TsMatch => (BinOp::TsMatch, 5),
25877        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
25878        // PG places these at the comparison rung (same level as `=`),
25879        // so we follow.
25880        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
25881        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
25882        Token::InetContains => (BinOp::InetContains, 5),
25883        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
25884        Token::InetOverlap => (BinOp::InetOverlap, 5),
25885        // v7.39 (round 508) — the geometric and pattern-order predicates
25886        // ride the comparison rung, as every other predicate does.
25887        Token::Intersects => (BinOp::Intersects, 5),
25888        Token::IsBelow => (BinOp::IsBelow, 5),
25889        Token::IsAbove => (BinOp::IsAbove, 5),
25890        Token::PatternLt => (BinOp::PatternLt, 5),
25891        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
25892        Token::PatternGt => (BinOp::PatternGt, 5),
25893        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
25894        // `@@@` is the old spelling of `@@` and means exactly it.
25895        Token::TsMatchOld => (BinOp::TsMatch, 5),
25896        _ => return None,
25897    };
25898    Some(pair)
25899}
25900
25901#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
25902// `as f32` here is intentional: vector elements widen / narrow into f32 on
25903// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
25904// past ~15 decimal digits — both are acceptable for a fixed-precision
25905// pgvector column.
25906/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
25907/// implicit table alias and break trailing clauses. WITH lands
25908/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
25909/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
25910/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
25911/// / VALUES / FOR / LATERAL — all of which would otherwise be
25912/// silently swallowed by `parse_optional_alias`.
25913fn is_alias_stopword(s: &str) -> bool {
25914    matches!(
25915        s.to_ascii_lowercase().as_str(),
25916        "with"
25917            | "on"
25918            | "where"
25919            | "having"
25920            | "group"
25921            | "order"
25922            | "limit"
25923            | "offset"
25924            | "union"
25925            | "except"
25926            | "intersect"
25927            | "returning"
25928            | "set"
25929            | "values"
25930            | "for"
25931            | "window"
25932            | "tablesample"
25933            | "lateral"
25934            | "left"
25935            | "right"
25936            | "inner"
25937            | "outer"
25938            | "full"
25939            | "cross"
25940            | "join"
25941            | "natural"
25942            | "using"
25943            | "fetch"
25944    )
25945}
25946
25947fn extract_numeric_literal(e: &Expr) -> Option<f32> {
25948    match e {
25949        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
25950        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
25951        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
25952        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
25953        // so scale the divisor by hand instead of `f32::powi`.)
25954        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
25955            let mut div = 1.0f32;
25956            for _ in 0..*scale {
25957                div *= 10.0;
25958            }
25959            Some(*unscaled as f32 / div)
25960        }
25961        Expr::Unary {
25962            op: UnOp::Neg,
25963            expr,
25964        } => extract_numeric_literal(expr).map(|x| -x),
25965        _ => None,
25966    }
25967}
25968
25969/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
25970/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
25971/// negative. Returns `None` if any pair fails to parse or no pair is found.
25972///
25973/// Recognised units (case-insensitive, optional trailing `s`):
25974/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
25975/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
25976/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
25977/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
25978/// (PG-canonical: DST and month-boundary semantics depend on this).
25979/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
25980/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
25981/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
25982#[allow(clippy::cast_possible_truncation)]
25983fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
25984    let mut months: i64 = 0;
25985    let mut days: i64 = 0;
25986    let mut micros: i64 = 0;
25987    let mut in_time = false;
25988    let mut num = alloc::string::String::new();
25989    for ch in rest.chars() {
25990        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
25991            num.push(ch);
25992            continue;
25993        }
25994        if ch == 'T' || ch == 't' {
25995            if !num.is_empty() {
25996                return None;
25997            }
25998            in_time = true;
25999            continue;
26000        }
26001        let n: f64 = num.parse().ok()?;
26002        num.clear();
26003        match (ch, in_time) {
26004            ('Y' | 'y', false) => months += (n * 12.0) as i64,
26005            ('M', false) => months += n as i64,
26006            ('W' | 'w', false) => days += (n * 7.0) as i64,
26007            ('D' | 'd', false) => days += n as i64,
26008            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26009            ('M', true) => micros += (n * 60_000_000.0) as i64,
26010            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26011            _ => return None,
26012        }
26013    }
26014    if !num.is_empty() {
26015        return None;
26016    }
26017    Some((
26018        i32::try_from(months).ok()?,
26019        i32::try_from(days).ok()?,
26020        micros,
26021    ))
26022}
26023
26024/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26025/// leading `-` negates the whole value). Rejects date-like strings.
26026fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26027    let (neg, body) = match s.strip_prefix('-') {
26028        Some(b) => (true, b),
26029        None => (false, s),
26030    };
26031    let (y, m) = body.split_once('-')?;
26032    let years: i32 = y.parse().ok()?;
26033    let mons: i32 = m.parse().ok()?;
26034    if years < 0 || mons < 0 {
26035        return None;
26036    }
26037    let total = years.checked_mul(12)?.checked_add(mons)?;
26038    Some((if neg { -total } else { total }, 0, 0))
26039}
26040
26041/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26042/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26043fn parse_interval_clock(tok: &str) -> Option<i64> {
26044    let (neg, body) = match tok.strip_prefix('-') {
26045        Some(r) => (true, r),
26046        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26047    };
26048    let mut it = body.split(':');
26049    let h: i64 = it.next()?.parse().ok()?;
26050    let m: i64 = it.next()?.parse().ok()?;
26051    let s_tok = it.next().unwrap_or("0");
26052    if it.next().is_some() {
26053        return None;
26054    }
26055    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26056        let sec: i64 = sec.parse().ok()?;
26057        let mut f = alloc::string::String::from(frac);
26058        while f.len() < 6 {
26059            f.push('0');
26060        }
26061        f.truncate(6);
26062        let fus: i64 = f.parse().ok()?;
26063        sec.checked_mul(1_000_000)?.checked_add(fus)?
26064    } else {
26065        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26066    };
26067    let total = h
26068        .checked_mul(3_600_000_000)?
26069        .checked_add(m.checked_mul(60_000_000)?)?
26070        .checked_add(sec_us)?;
26071    Some(if neg { -total } else { total })
26072}
26073
26074/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26075/// every spelling PG accepts (measured against live PG18.4, not guessed):
26076/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26077/// Before this, the unit table matched long names only, with an ad-hoc
26078/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26079/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26080/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26081/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26082/// fractional) both read from this one table now.
26083fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26084    let u = raw.to_ascii_lowercase();
26085    Some(match u.as_str() {
26086        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26087            "microsecond"
26088        }
26089        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26090            "millisecond"
26091        }
26092        "second" | "seconds" | "sec" | "secs" | "s" => "second",
26093        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26094        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26095        "day" | "days" | "d" => "day",
26096        "week" | "weeks" | "w" => "week",
26097        "month" | "months" | "mon" | "mons" => "month",
26098        "year" | "years" | "yr" | "yrs" | "y" => "year",
26099        "decade" | "decades" | "dec" | "decs" => "decade",
26100        "century" | "centuries" | "cent" | "c" => "century",
26101        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26102        _ => return None,
26103    })
26104}
26105
26106/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26107/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26109pub(crate) enum IntervalField {
26110    Year,
26111    Month,
26112    Day,
26113    Hour,
26114    Minute,
26115    Second,
26116}
26117
26118/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26119/// spellings aren't standard for the qualifier position, so only the singular
26120/// forms are accepted.
26121/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26122/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26123/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26124/// take a `'1 2'` style literal — are not read here; they stay a parse
26125/// error rather than being silently misread.)
26126/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26127///
26128/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26129/// to do with a `@@` engine setting, and an unset one reads NULL rather
26130/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26131/// were the same node and `SELECT @x` answered "Unknown system variable".)
26132/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26133/// not see a session override — measured, after `SET autocommit=0`,
26134/// `@@global.autocommit` is still 1.
26135///
26136/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26137/// the parser's nesting budget is tuned against, and building these
26138/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26139/// wall `parse_left_right_atom` and friends were factored out for).
26140#[inline(never)]
26141fn variable_ref_atom(raw: &str) -> Expr {
26142    let user_var = !raw.starts_with("@@");
26143    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26144    Expr::FunctionCall {
26145        name: String::from(if user_var {
26146            "__spg_user_var"
26147        } else {
26148            "__spg_session_var"
26149        }),
26150        args: alloc::vec![Expr::Literal(Literal::String(bare))],
26151    }
26152}
26153
26154fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26155    let Token::Ident(s) = tok else { return None };
26156    Some(match () {
26157        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26158        () if s.eq_ignore_ascii_case("second") => "second",
26159        () if s.eq_ignore_ascii_case("minute") => "minute",
26160        () if s.eq_ignore_ascii_case("hour") => "hour",
26161        () if s.eq_ignore_ascii_case("day") => "day",
26162        () if s.eq_ignore_ascii_case("week") => "week",
26163        () if s.eq_ignore_ascii_case("month") => "month",
26164        () if s.eq_ignore_ascii_case("quarter") => "quarter",
26165        () if s.eq_ignore_ascii_case("year") => "year",
26166        () => return None,
26167    })
26168}
26169
26170/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26171/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26172/// which constructs the value at run time. Only the slot the unit names
26173/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26174/// slot the builtin has (months and fractional seconds respectively).
26175fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26176    let zero = || Expr::Literal(Literal::Integer(0));
26177    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26178        lhs: alloc::boxed::Box::new(qty.clone()),
26179        op,
26180        rhs: alloc::boxed::Box::new(by),
26181    };
26182    // (years, months, weeks, days, hours, mins, secs)
26183    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26184    match unit {
26185        "year" => args[0] = qty,
26186        "quarter" => {
26187            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26188        }
26189        "month" => args[1] = qty,
26190        "week" => args[2] = qty,
26191        "day" => args[3] = qty,
26192        "hour" => args[4] = qty,
26193        "minute" => args[5] = qty,
26194        "second" => args[6] = qty,
26195        // The builtin's seconds slot takes a fraction, so microseconds ride
26196        // it scaled down; the divisor is a NUMERIC literal so the division
26197        // stays exact rather than going through a float.
26198        "microsecond" => {
26199            args[6] = scaled(
26200                crate::ast::BinOp::Div,
26201                Expr::Literal(Literal::Numeric {
26202                    unscaled: 1_000_000,
26203                    scale: 0,
26204                }),
26205            );
26206        }
26207        _ => args[3] = qty,
26208    }
26209    Expr::FunctionCall {
26210        name: alloc::string::String::from("make_interval"),
26211        args,
26212    }
26213}
26214
26215/// `(count, unit)` → `(months, days, micros)`.
26216fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26217    let n: i64 = count.trim().parse().ok()?;
26218    Some(match unit {
26219        "microsecond" => (0, 0, n),
26220        "second" => (0, 0, n.checked_mul(1_000_000)?),
26221        "minute" => (0, 0, n.checked_mul(60_000_000)?),
26222        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26223        "day" => (0, i32::try_from(n).ok()?, 0),
26224        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26225        "month" => (i32::try_from(n).ok()?, 0, 0),
26226        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26227        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26228        _ => return None,
26229    })
26230}
26231
26232fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26233    let Token::Ident(s) = tok else { return None };
26234    Some(match () {
26235        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26236        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26237        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26238        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26239        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26240        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26241        () => return None,
26242    })
26243}
26244
26245/// v7.39 (read01 round 102) — interpret an interval literal under a field
26246/// qualifier. Returns `(months, days, micros)`.
26247///
26248/// * A single field applied to a bare number sets which unit the number means,
26249///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26250///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26251/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26252/// * Every other range, and any literal a single field can't read as a plain
26253///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26254///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26255///   like PG, and the qualifier there only bounds precision.
26256fn interpret_qualified_interval(
26257    text: &str,
26258    (f1, f2): (IntervalField, Option<IntervalField>),
26259) -> Option<(i32, i32, i64)> {
26260    if let Some(f2) = f2 {
26261        if f1 == IntervalField::Year && f2 == IntervalField::Month {
26262            if let Some(m) = parse_year_month_literal(text) {
26263                return Some((m, 0, 0));
26264            }
26265        }
26266        return parse_interval_text(text);
26267    }
26268    // Single field: reinterpret a bare number; otherwise the default parse.
26269    let trimmed = text.trim();
26270    if let Ok(val) = trimmed.parse::<f64>() {
26271        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26272        #[allow(clippy::cast_possible_truncation)]
26273        let whole = val as i64;
26274        #[allow(clippy::cast_possible_truncation)]
26275        let secs_micros = {
26276            let m = val * 1_000_000.0;
26277            if m >= 0.0 {
26278                (m + 0.5) as i64
26279            } else {
26280                (m - 0.5) as i64
26281            }
26282        };
26283        return Some(match f1 {
26284            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26285            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26286            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26287            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26288            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26289            IntervalField::Second => (0, 0, secs_micros),
26290        });
26291    }
26292    parse_interval_text(text)
26293}
26294
26295/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26296fn parse_year_month_literal(text: &str) -> Option<i32> {
26297    let t = text.trim();
26298    let (neg, body) = match t.strip_prefix('-') {
26299        Some(r) => (true, r),
26300        None => (false, t.strip_prefix('+').unwrap_or(t)),
26301    };
26302    let mut it = body.split('-');
26303    let years: i32 = it.next()?.trim().parse().ok()?;
26304    let months: i32 = match it.next() {
26305        Some(m) => m.trim().parse().ok()?,
26306        None => 0,
26307    };
26308    if it.next().is_some() {
26309        return None;
26310    }
26311    let total = years.checked_mul(12)?.checked_add(months)?;
26312    Some(if neg { -total } else { total })
26313}
26314
26315pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26316    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26317    // `@` is decorative; a trailing `ago` negates the whole interval.
26318    let mut trimmed = s.trim();
26319    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26320    let mut negate = false;
26321    if let Some(rest) = trimmed
26322        .strip_suffix("ago")
26323        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26324    {
26325        negate = true;
26326        trimmed = rest.trim();
26327    }
26328    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26329        let (mo, d, us) = v?;
26330        if negate {
26331            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26332        } else {
26333            Some((mo, d, us))
26334        }
26335    };
26336    let s = trimmed;
26337    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26338    // are single tokens, not the `<n> <unit>` pair form handled below.
26339    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26340        return finish(parse_iso8601_interval(rest));
26341    }
26342    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26343        if let Some(iv) = parse_year_month_interval(trimmed) {
26344            return finish(Some(iv));
26345        }
26346    }
26347    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26348    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26349    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26350    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26351        if let Ok(n) = trimmed.parse::<i64>() {
26352            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26353        }
26354        if let Ok(f) = trimmed.parse::<f64>() {
26355            if f.is_finite() {
26356                #[allow(clippy::cast_possible_truncation)]
26357                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26358            }
26359        }
26360    }
26361    // v7.39 (round 243) — PG accepts the number and unit run together
26362    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26363    // the `<n> <unit>` pair loop below sees them as two.
26364    let raw_parts: Vec<&str> = s.split_whitespace().collect();
26365    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26366    for p in raw_parts {
26367        let boundary = p
26368            .char_indices()
26369            .find(|(i, c)| {
26370                *i > 0
26371                    && c.is_ascii_alphabetic()
26372                    && p[..*i]
26373                        .chars()
26374                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26375                    && p[..*i].chars().any(|d| d.is_ascii_digit())
26376            })
26377            .map(|(i, _)| i);
26378        match boundary {
26379            Some(i) => {
26380                parts.push(&p[..i]);
26381                parts.push(&p[i..]);
26382            }
26383            None => parts.push(p),
26384        }
26385    }
26386    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26387    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26388    // remains is the `<n> <unit>` pair form handled below.
26389    let mut clock_us: i64 = 0;
26390    let mut had_clock = false;
26391    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26392        clock_us = parse_interval_clock(parts[pos])?;
26393        parts.remove(pos);
26394        had_clock = true;
26395    }
26396    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26397    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26398    let mut lone_days: i32 = 0;
26399    if had_clock && parts.len() == 1 {
26400        if let Ok(n) = parts[0].parse::<i64>() {
26401            lone_days = i32::try_from(n).ok()?;
26402            parts.clear();
26403        }
26404    }
26405    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26406        return None;
26407    }
26408    let mut months: i32 = 0;
26409    let mut days: i32 = lone_days;
26410    let mut micros: i64 = clock_us;
26411    let mut i = 0;
26412    while i < parts.len() {
26413        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26414        if let Ok(n) = parts[i].parse::<i64>() {
26415            match unit_stripped {
26416                "microsecond" => micros = micros.checked_add(n)?,
26417                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26418                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26419                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26420                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26421                "day" => {
26422                    let n32 = i32::try_from(n).ok()?;
26423                    days = days.checked_add(n32)?;
26424                }
26425                "week" => {
26426                    let n32 = i32::try_from(n).ok()?;
26427                    days = days.checked_add(n32.checked_mul(7)?)?;
26428                }
26429                "month" => {
26430                    let n32 = i32::try_from(n).ok()?;
26431                    months = months.checked_add(n32)?;
26432                }
26433                "year" => {
26434                    let n32 = i32::try_from(n).ok()?;
26435                    months = months.checked_add(n32.checked_mul(12)?)?;
26436                }
26437                // v7.39 (read01 timestamp.c) — the larger calendar units.
26438                "decade" => {
26439                    let n32 = i32::try_from(n).ok()?;
26440                    months = months.checked_add(n32.checked_mul(120)?)?;
26441                }
26442                "century" => {
26443                    let n32 = i32::try_from(n).ok()?;
26444                    months = months.checked_add(n32.checked_mul(1200)?)?;
26445                }
26446                "millennium" => {
26447                    let n32 = i32::try_from(n).ok()?;
26448                    months = months.checked_add(n32.checked_mul(12000)?)?;
26449                }
26450                _ => return None,
26451            }
26452        } else if let Ok(f) = parts[i].parse::<f64>() {
26453            // Fractional units cascade down to the next-finer field the way
26454            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26455            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26456            // no_std: f64 has no trunc/fract/round methods, so do them with
26457            // casts (toward-zero) + explicit round-half-away-from-zero.
26458            #[allow(clippy::cast_possible_truncation)]
26459            fn round_i64(x: f64) -> i64 {
26460                if x >= 0.0 {
26461                    (x + 0.5) as i64
26462                } else {
26463                    (x - 0.5) as i64
26464                }
26465            }
26466            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26467            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26468                const DAY_US: f64 = 86_400_000_000.0;
26469                let whole = d as i64; // truncates toward zero
26470                let frac = d - whole as f64;
26471                *days = days.checked_add(i32::try_from(whole).ok()?)?;
26472                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26473                Some(())
26474            }
26475            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26476            match unit_stripped {
26477                "microsecond" => micros = micros.checked_add(round_i64(f))?,
26478                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26479                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26480                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26481                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26482                "day" => add_days_frac(&mut days, &mut micros, f)?,
26483                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26484                "month" => {
26485                    let whole = f as i64;
26486                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26487                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26488                }
26489                "year" => {
26490                    let m = f * 12.0;
26491                    let whole = m as i64;
26492                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26493                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26494                }
26495                _ => return None,
26496            }
26497        } else {
26498            return None;
26499        }
26500        i += 2;
26501    }
26502    finish(Some((months, days, micros)))
26503}
26504
26505/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26506/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26507/// `interval` is intentionally absent (handled by its own parser arm).
26508/// Returns `None` for names that aren't sensible as a bare typed literal, so
26509/// the caller falls back to treating the ident as a column reference.
26510fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26511    Some(match ident {
26512        "date" => CastTarget::Date,
26513        "timestamp" | "datetime" => CastTarget::Timestamp,
26514        "timestamptz" => CastTarget::Timestamptz,
26515        "bool" | "boolean" => CastTarget::Bool,
26516        "int" | "integer" | "int4" => CastTarget::Int,
26517        "bigint" | "int8" => CastTarget::BigInt,
26518        "float8" | "double precision" => CastTarget::Float,
26519        "uuid" => CastTarget::Uuid,
26520        "bytea" => CastTarget::Bytea,
26521        "json" => CastTarget::Json,
26522        "jsonb" => CastTarget::Jsonb,
26523        // Types without a dedicated CastTarget variant flow through the
26524        // generic Named path (engine resolves via column_type_to_data_type).
26525        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26526        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26527        | "money" | "bit" | "varbit"
26528        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26529        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26530        // Range / multirange types likewise.
26531        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26532        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26533        | "datemultirange" | "tsmultirange" | "tstzmultirange"
26534        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26535        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26536            CastTarget::Named(alloc::string::String::from(ident))
26537        }
26538        _ => return None,
26539    })
26540}
26541
26542/// v7.12.4 — map a bare type-name identifier (the form that
26543/// appears in a function arg list or RETURNS clause) to a
26544/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26545/// types so the caller can preserve them as
26546/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26547///
26548/// Subset of the full column-type grammar — we deliberately
26549/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26550/// here because function-arg types in v7.12.4 are mostly the
26551/// bare form (`text`, `int`, `bytea`, …).
26552/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
26553/// than being `name TYPE`?
26554///
26555/// The multi-word spellings SQL allows for a bare argument type, each
26556/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
26557///
26558/// NOTE this list also exists in `spg-storage`, which computes the
26559/// signature key from the rendered argument text and has to reach the
26560/// same verdict. The two crates are siblings — neither depends on the
26561/// other — and each already carries its own table of type spellings
26562/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
26563/// there), so this follows the structure rather than inventing new
26564/// duplication. Recorded as V49.
26565pub fn is_multiword_type_phrase(phrase: &str) -> bool {
26566    let t = phrase.trim().to_ascii_lowercase();
26567    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
26568    matches!(
26569        base,
26570        "double precision"
26571            | "character varying"
26572            | "bit varying"
26573            | "timestamp with time zone"
26574            | "timestamp without time zone"
26575            | "time with time zone"
26576            | "time without time zone"
26577            | "national character"
26578            | "national character varying"
26579    )
26580}
26581
26582fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
26583    Some(match ident.to_ascii_lowercase().as_str() {
26584        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
26585        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
26586        "bigint" => ColumnTypeName::BigInt,
26587        "float" | "double" => ColumnTypeName::Float,
26588        // v7.39 (round 269) — real is 32-bit.
26589        "real" | "float4" => ColumnTypeName::Real,
26590        "text" => ColumnTypeName::Text,
26591        "bool" | "boolean" => ColumnTypeName::Bool,
26592        "date" => ColumnTypeName::Date,
26593        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
26594        "timestamptz" => ColumnTypeName::Timestamptz,
26595        "json" => ColumnTypeName::Json,
26596        "jsonb" => ColumnTypeName::Jsonb,
26597        "bytea" | "bytes" => ColumnTypeName::Bytes,
26598        "tsvector" => ColumnTypeName::TsVector,
26599        "tsquery" => ColumnTypeName::TsQuery,
26600        "uuid" => ColumnTypeName::Uuid,
26601        "interval" => ColumnTypeName::Interval,
26602        "time" => ColumnTypeName::Time,
26603        "year" => ColumnTypeName::Year,
26604        "timetz" => ColumnTypeName::TimeTz,
26605        "money" => ColumnTypeName::Money,
26606        _ => return None,
26607    })
26608}
26609
26610/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
26611/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
26612///
26613/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
26614/// / embedded SQL land in v7.12.5+):
26615///
26616/// ```text
26617///   body          := [ws] block [ws]
26618///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
26619///   stmt          := assign | return
26620///   assign        := assign_target := expr
26621///   assign_target := ( NEW | OLD ) . ident | ident
26622///   return        := RETURN ( NEW | OLD | NULL | expr )
26623/// ```
26624///
26625/// `expr` is parsed by recursing into the regular `Parser` — so a
26626/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
26627/// NEW.subject || ' ' || NEW.sender)` body shape works without
26628/// the body parser knowing what `to_tsvector` is.
26629///
26630/// Errors here cause the caller to fall back to
26631/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
26632/// successful, but the executor will refuse to invoke the
26633/// function with an "unparseable body" error.
26634/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
26635/// from the crate root as `spg_sql::parse_function_body`.
26636pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26637    parse_plpgsql_body(body)
26638}
26639
26640fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26641    // Use the regular lexer on the body text. The trailing
26642    // `END;` may or may not have a semicolon; the lexer treats
26643    // both forms identically.
26644    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
26645        message: alloc::format!("plpgsql body lex error: {e}"),
26646        token_pos: 0,
26647    })?;
26648    let mut parser = Parser::new(tokens);
26649    parser.parse_plpgsql_block()
26650}
26651
26652/// v7.39 (GUC) — the textual body of a SET value, for list joining.
26653fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
26654    match v {
26655        crate::ast::SetValue::String(s)
26656        | crate::ast::SetValue::Ident(s)
26657        | crate::ast::SetValue::Number(s) => s.clone(),
26658        crate::ast::SetValue::Default => "DEFAULT".into(),
26659    }
26660}
26661
26662/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
26663/// contains an aggregate call at ITS OWN query level (recursion stops at
26664/// sublink boundaries — a sublink's aggregates belong to the sublink).
26665/// Backs the "aggregate functions are not allowed in a recursive query's
26666/// recursive term" well-formedness check.
26667fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
26668    const AGG_NAMES: &[&str] = &[
26669        "count",
26670        "sum",
26671        "min",
26672        "max",
26673        "avg",
26674        "string_agg",
26675        "array_agg",
26676        "bool_and",
26677        "bool_or",
26678        "every",
26679        "any_value",
26680        "json_agg",
26681        "jsonb_agg",
26682        "json_object_agg",
26683        "jsonb_object_agg",
26684        "bit_and",
26685        "bit_or",
26686        "bit_xor",
26687        "var_pop",
26688        "var_samp",
26689        "variance",
26690        "stddev",
26691        "stddev_pop",
26692        "stddev_samp",
26693        "range_agg",
26694        "range_intersect_agg",
26695        "percentile_cont",
26696        "percentile_disc",
26697        "mode",
26698        "corr",
26699        "covar_pop",
26700        "covar_samp",
26701    ];
26702    match e {
26703        Expr::AggregateOrdered { .. } => true,
26704        Expr::FunctionCall { name, args } => {
26705            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
26706                || args.iter().any(expr_has_toplevel_aggregate)
26707        }
26708        Expr::NamedArg { expr, .. }
26709        | Expr::Variadic(expr)
26710        | Expr::Unary { expr, .. }
26711        | Expr::Cast { expr, .. }
26712        | Expr::IsNull { expr, .. }
26713        | Expr::FieldAccess { base: expr, .. }
26714        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
26715        Expr::Binary { lhs, rhs, .. } => {
26716            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
26717        }
26718        Expr::Like { expr, pattern, .. } => {
26719            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
26720        }
26721        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
26722        Expr::InList { expr, list, .. } => {
26723            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
26724        }
26725        Expr::ArraySubscript { target, index } => {
26726            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
26727        }
26728        Expr::ArraySlice { target, lo, hi } => {
26729            expr_has_toplevel_aggregate(target)
26730                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
26731                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
26732        }
26733        Expr::AnyAll { expr, array, .. } => {
26734            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
26735        }
26736        Expr::Case {
26737            operand,
26738            branches,
26739            else_branch,
26740        } => {
26741            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
26742                || branches
26743                    .iter()
26744                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
26745                || else_branch
26746                    .as_deref()
26747                    .is_some_and(expr_has_toplevel_aggregate)
26748        }
26749        // The outer-level operands of a sublink can aggregate; the sublink's
26750        // own body cannot leak its aggregates up here.
26751        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
26752        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
26753            row.iter().any(expr_has_toplevel_aggregate)
26754        }
26755        _ => false,
26756    }
26757}
26758
26759/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
26760/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
26761/// named table anywhere in its subtree. A plain FROM derived table is NOT a
26762/// sublink and is legal in a recursive term, so it is not walked here.
26763fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
26764    let mut exprs: Vec<&Expr> = Vec::new();
26765    for it in &s.items {
26766        if let crate::ast::SelectItem::Expr { expr, .. } = it {
26767            exprs.push(expr);
26768        }
26769    }
26770    if let Some(w) = &s.where_ {
26771        exprs.push(w);
26772    }
26773    if let Some(h) = &s.having {
26774        exprs.push(h);
26775    }
26776    if let Some(g) = &s.group_by {
26777        exprs.extend(g.iter());
26778    }
26779    if let Some(from) = &s.from {
26780        for j in &from.joins {
26781            if let Some(on) = &j.on {
26782                exprs.push(on);
26783            }
26784        }
26785    }
26786    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
26787}
26788
26789/// Does this expression contain a sublink whose subquery mentions `name`?
26790fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
26791    match e {
26792        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
26793        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
26794        Expr::InSubquery { expr, subquery, .. } => {
26795            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
26796        }
26797        Expr::RowInSubquery { row, subquery, .. } => {
26798            row.iter().any(|x| expr_sublink_mentions(x, name))
26799                || select_mentions_table(subquery, name)
26800        }
26801        Expr::RowCmpSubquery { row, subquery, .. } => {
26802            row.iter().any(|x| expr_sublink_mentions(x, name))
26803                || select_mentions_table(subquery, name)
26804        }
26805        Expr::NamedArg { expr, .. }
26806        | Expr::Variadic(expr)
26807        | Expr::Unary { expr, .. }
26808        | Expr::Cast { expr, .. }
26809        | Expr::IsNull { expr, .. }
26810        | Expr::FieldAccess { base: expr, .. }
26811        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
26812        Expr::Binary { lhs, rhs, .. } => {
26813            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
26814        }
26815        Expr::Like { expr, pattern, .. } => {
26816            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
26817        }
26818        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
26819            args.iter().any(|x| expr_sublink_mentions(x, name))
26820        }
26821        Expr::InList { expr, list, .. } => {
26822            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
26823        }
26824        Expr::ArraySubscript { target, index } => {
26825            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
26826        }
26827        Expr::ArraySlice { target, lo, hi } => {
26828            expr_sublink_mentions(target, name)
26829                || lo
26830                    .as_deref()
26831                    .is_some_and(|x| expr_sublink_mentions(x, name))
26832                || hi
26833                    .as_deref()
26834                    .is_some_and(|x| expr_sublink_mentions(x, name))
26835        }
26836        Expr::AnyAll { expr, array, .. } => {
26837            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
26838        }
26839        Expr::Case {
26840            operand,
26841            branches,
26842            else_branch,
26843        } => {
26844            operand
26845                .as_deref()
26846                .is_some_and(|x| expr_sublink_mentions(x, name))
26847                || branches
26848                    .iter()
26849                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
26850                || else_branch
26851                    .as_deref()
26852                    .is_some_and(|x| expr_sublink_mentions(x, name))
26853        }
26854        _ => false,
26855    }
26856}
26857
26858/// Does this SELECT (in full — FROM tables, derived tables, its own
26859/// sublinks, and union arms) mention the named table?
26860fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
26861    if let Some(from) = &s.from {
26862        if from.primary.name.eq_ignore_ascii_case(name) {
26863            return true;
26864        }
26865        if let Some(sub) = &from.primary.lateral_subquery
26866            && select_mentions_table(sub, name)
26867        {
26868            return true;
26869        }
26870        for j in &from.joins {
26871            if j.table.name.eq_ignore_ascii_case(name) {
26872                return true;
26873            }
26874            if let Some(sub) = &j.table.lateral_subquery
26875                && select_mentions_table(sub, name)
26876            {
26877                return true;
26878            }
26879        }
26880    }
26881    if select_has_self_ref_in_sublink(s, name) {
26882        return true;
26883    }
26884    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
26885}
26886
26887/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
26888/// row count, the way PG evaluates one before applying it.
26889///
26890/// `None` = not a constant (a column, a subquery, a function call).
26891/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
26892/// message stands in for LIMIT / OFFSET, which the caller substitutes.
26893/// All wordings were read off live PG 18.4.
26894fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
26895    use crate::ast::{BinOp, Expr, Literal, UnOp};
26896    match e {
26897        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
26898        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26899            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
26900        }
26901        // PG coerces a string by its CONTENT, and fails on the value.
26902        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
26903            |_| {
26904                Err(alloc::format!(
26905                    "invalid input syntax for type bigint: \"{t}\""
26906                ))
26907            },
26908            |n| Ok(i128::from(n)),
26909        )),
26910        Expr::Literal(Literal::Bool(_)) => Some(Err(
26911            "argument of {L} must be type bigint, not type boolean".into(),
26912        )),
26913        Expr::Unary {
26914            op: UnOp::Neg,
26915            expr,
26916        } => match fold_limit_constant(expr)? {
26917            Ok(v) => Some(Ok(-v)),
26918            e @ Err(_) => Some(e),
26919        },
26920        Expr::Binary { lhs, op, rhs } => {
26921            let a = match fold_limit_constant(lhs)? {
26922                Ok(v) => v,
26923                e @ Err(_) => return Some(e),
26924            };
26925            let b = match fold_limit_constant(rhs)? {
26926                Ok(v) => v,
26927                e @ Err(_) => return Some(e),
26928            };
26929            let out = match op {
26930                BinOp::Add => a.checked_add(b),
26931                BinOp::Sub => a.checked_sub(b),
26932                BinOp::Mul => a.checked_mul(b),
26933                BinOp::Div if b != 0 => a.checked_div(b),
26934                BinOp::Div => return Some(Err("division by zero".into())),
26935                BinOp::Mod if b != 0 => a.checked_rem(b),
26936                BinOp::Mod => return Some(Err("division by zero".into())),
26937                _ => return None,
26938            };
26939            // PG evaluates the arithmetic in the operand's own type, so an
26940            // int-by-int product that leaves int range fails there — before
26941            // the row count is ever looked at.
26942            match out {
26943                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
26944                    Some(Err("integer out of range".into()))
26945                }
26946                Some(v) => Some(Ok(v)),
26947                None => Some(Err("integer out of range".into())),
26948            }
26949        }
26950        _ => None,
26951    }
26952}
26953
26954/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
26955/// cast, which is what makes `LIMIT 2.5` keep three rows.
26956fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
26957    if scale == 0 {
26958        return unscaled;
26959    }
26960    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
26961        return 0;
26962    };
26963    let neg = unscaled < 0;
26964    let mag = unscaled.unsigned_abs() as i128;
26965    let rounded = (mag + div / 2) / div;
26966    if neg { -rounded } else { rounded }
26967}
26968
26969#[cfg(test)]
26970mod tests {
26971    use super::*;
26972    use alloc::string::ToString;
26973
26974    fn parse(s: &str) -> Statement {
26975        parse_statement(s).expect("parse ok")
26976    }
26977
26978    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
26979    // `tables`, `partition`, etc. are unreserved keywords per PG's
26980    // `pg_get_keywords()` and MUST be usable as column / table /
26981    // alias names. Pre-T4 every drop-in user whose schema had one
26982    // of these as a column name (sentori events.release, mailrs
26983    // messages.index in some forks) blew the parser up at CREATE
26984    // TABLE time with "expected identifier, got Release". The
26985    // generalisation lives in `unreserved_keyword_text` + the
26986    // `expect_ident_like` and `parse_atom` arms that consult it.
26987    #[test]
26988    fn release_usable_as_column_name_in_create_table() {
26989        let stmt =
26990            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
26991        if let Statement::CreateTable(t) = stmt {
26992            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
26993            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
26994        } else {
26995            panic!("expected CreateTable");
26996        }
26997    }
26998
26999    #[test]
27000    fn release_usable_as_column_ref_in_select_projection() {
27001        // The sentori `0003_partition_events.sql` INSERT-SELECT
27002        // walk references `release` in both column lists; the
27003        // projection-side use exercises `parse_atom`'s relaxed
27004        // identifier set.
27005        parse("SELECT id, release, payload FROM events WHERE id = 1");
27006    }
27007
27008    #[test]
27009    fn release_usable_as_column_ref_in_insert_column_list() {
27010        // INSERT INTO t (id, release, payload) VALUES (…)
27011        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27012    }
27013
27014    #[test]
27015    fn alter_column_drop_not_null_uses_keyword_drop_token() {
27016        // Sentori `0013_audit_tombstone.sql` issues
27017        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27018        // emits Token::Drop (not Ident("drop")); the parser must
27019        // accept both in the ALTER COLUMN sub-dispatch.
27020        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27021    }
27022
27023    #[test]
27024    fn create_index_accepts_parenthesised_expression_key() {
27025        // sentori `0040_events_bundle_idx.sql` shape — JSONB
27026        // expression index. Pre-T4 the parser bailed at the
27027        // inner `(` with "expected column ident or expression,
27028        // got LParen". The Token::LParen arm in CREATE INDEX
27029        // routes through the expression parser instead.
27030        parse(
27031            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27032             ON events ((payload->'bundle'->>'id'))",
27033        );
27034    }
27035
27036    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27037    // surface as parse errors, never stack overflows (embed hosts
27038    // abort on overflow).
27039    /// The nesting budget is a COUNT; what it has to fit inside is a
27040    /// number of BYTES, and only one of those two is stable across
27041    /// compiler versions. Round 847 measured 30,336 bytes per level
27042    /// after a toolchain move, which puts 64 levels at 1.94 MB and
27043    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27044    /// aborted instead of erroring, which is precisely the outcome it
27045    /// exists to rule out.
27046    ///
27047    /// So the budget is metered rather than assumed. The ceiling leaves
27048    /// the depth SPG advertises fitting in a default 2 MiB thread with
27049    /// room to spare, in the debug build, where frames are widest.
27050    #[test]
27051    fn nesting_frame_cost_stays_under_ceiling() {
27052        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27053        // thread keeps a margin for whatever called the parser.
27054        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27055
27056        frame_meter::reset();
27057        let depth = frame_meter::SAMPLE_HI + 8;
27058        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27059        parse(&sql);
27060
27061        let per_level = frame_meter::bytes_per_level();
27062        {
27063            extern crate std;
27064            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27065        }
27066        assert!(
27067            per_level <= CEILING,
27068            "{per_level} bytes per nesting level exceeds {CEILING}; \
27069             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27070             in parse_expr_inner / parse_unary rather than lowering the \
27071             depth or widening the stack.",
27072            per_level * MAX_NEST_DEPTH
27073        );
27074    }
27075
27076    #[test]
27077    fn nesting_budget_errors_cleanly() {
27078        let depth = MAX_NEST_DEPTH + 50;
27079        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27080        let err = parse_statement(&sql).expect_err("must reject");
27081        assert!(err.message.contains("nests deeper"), "{err:?}");
27082        // Within budget still parses.
27083        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27084        parse(&sql);
27085    }
27086
27087    #[test]
27088    fn binary_chain_budget_errors_cleanly() {
27089        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27090        let err = parse_statement(&sql).expect_err("must reject");
27091        assert!(err.message.contains("chained binary"), "{err:?}");
27092        // Within budget still parses (chain depth ≤ budget is safe
27093        // for recursive eval/drop on 2 MiB stacks).
27094        let sql = format!("SELECT 1{}", " + 1".repeat(200));
27095        parse(&sql);
27096    }
27097
27098    #[test]
27099    fn in_list_unaffected_by_chain_budget() {
27100        // Flat InList: 20k elements parse fine and stay flat.
27101        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27102        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27103        let Statement::Select(s) = parse(&sql) else {
27104            panic!("expected select")
27105        };
27106        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27107            panic!("expected flat InList, got {:?}", s.where_)
27108        };
27109        assert_eq!(list.len(), 20_000);
27110        assert!(!negated);
27111    }
27112
27113    fn lit_int(n: i64) -> Expr {
27114        Expr::Literal(Literal::Integer(n))
27115    }
27116
27117    fn col(name: &str) -> Expr {
27118        Expr::Column(ColumnName {
27119            qualifier: None,
27120            name: name.into(),
27121        })
27122    }
27123
27124    #[test]
27125    fn select_single_integer() {
27126        let s = parse("SELECT 1");
27127        let Statement::Select(s) = s else {
27128            panic!("expected SELECT")
27129        };
27130        assert_eq!(s.items.len(), 1);
27131        assert!(s.from.is_none());
27132        assert!(s.where_.is_none());
27133    }
27134
27135    #[test]
27136    fn select_multiple_literal_kinds() {
27137        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27138        let Statement::Select(s) = s else {
27139            panic!("expected SELECT")
27140        };
27141        assert_eq!(s.items.len(), 5);
27142    }
27143
27144    #[test]
27145    fn select_wildcard_from_table() {
27146        let s = parse("SELECT * FROM users");
27147        let Statement::Select(s) = s else {
27148            panic!("expected SELECT")
27149        };
27150        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27151        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27152    }
27153
27154    #[test]
27155    fn select_with_table_alias() {
27156        let s = parse("SELECT * FROM users AS u");
27157        let Statement::Select(s) = s else {
27158            panic!("expected SELECT")
27159        };
27160        let t = &s.from.as_ref().unwrap().primary;
27161        assert_eq!(t.name, "users");
27162        assert_eq!(t.alias.as_deref(), Some("u"));
27163    }
27164
27165    #[test]
27166    fn select_with_where_eq() {
27167        let s = parse("SELECT a FROM t WHERE a = 1");
27168        let Statement::Select(s) = s else {
27169            panic!("expected SELECT")
27170        };
27171        let w = s.where_.unwrap();
27172        assert_eq!(
27173            w,
27174            Expr::Binary {
27175                lhs: Box::new(col("a")),
27176                op: BinOp::Eq,
27177                rhs: Box::new(lit_int(1)),
27178            }
27179        );
27180    }
27181
27182    #[test]
27183    fn arithmetic_precedence() {
27184        let s = parse("SELECT 1 + 2 * 3");
27185        let Statement::Select(s) = s else {
27186            panic!("expected SELECT")
27187        };
27188        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27189            panic!("wildcard?")
27190        };
27191        assert_eq!(
27192            expr,
27193            &Expr::Binary {
27194                lhs: Box::new(lit_int(1)),
27195                op: BinOp::Add,
27196                rhs: Box::new(Expr::Binary {
27197                    lhs: Box::new(lit_int(2)),
27198                    op: BinOp::Mul,
27199                    rhs: Box::new(lit_int(3)),
27200                }),
27201            }
27202        );
27203    }
27204
27205    #[test]
27206    fn parentheses_override_precedence() {
27207        let s = parse("SELECT (1 + 2) * 3");
27208        let Statement::Select(s) = s else {
27209            panic!("expected SELECT")
27210        };
27211        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27212            panic!()
27213        };
27214        assert_eq!(
27215            expr,
27216            &Expr::Binary {
27217                lhs: Box::new(Expr::Binary {
27218                    lhs: Box::new(lit_int(1)),
27219                    op: BinOp::Add,
27220                    rhs: Box::new(lit_int(2)),
27221                }),
27222                op: BinOp::Mul,
27223                rhs: Box::new(lit_int(3)),
27224            }
27225        );
27226    }
27227
27228    #[test]
27229    fn not_binds_below_comparison() {
27230        // `NOT a = 1` should parse as `NOT (a = 1)`.
27231        let s = parse("SELECT NOT a = 1 FROM t");
27232        let Statement::Select(s) = s else {
27233            panic!("expected SELECT")
27234        };
27235        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27236            panic!()
27237        };
27238        assert_eq!(
27239            expr,
27240            &Expr::Unary {
27241                op: UnOp::Not,
27242                expr: Box::new(Expr::Binary {
27243                    lhs: Box::new(col("a")),
27244                    op: BinOp::Eq,
27245                    rhs: Box::new(lit_int(1)),
27246                }),
27247            }
27248        );
27249    }
27250
27251    #[test]
27252    fn unary_minus_binds_above_multiplication() {
27253        // `-a * 2` should be `(-a) * 2`.
27254        let s = parse("SELECT -a * 2 FROM t");
27255        let Statement::Select(s) = s else {
27256            panic!("expected SELECT")
27257        };
27258        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27259            panic!()
27260        };
27261        assert_eq!(
27262            expr,
27263            &Expr::Binary {
27264                lhs: Box::new(Expr::Unary {
27265                    op: UnOp::Neg,
27266                    expr: Box::new(col("a")),
27267                }),
27268                op: BinOp::Mul,
27269                rhs: Box::new(lit_int(2)),
27270            }
27271        );
27272    }
27273
27274    #[test]
27275    fn qualified_column() {
27276        let s = parse("SELECT t.col FROM t");
27277        let Statement::Select(s) = s else {
27278            panic!("expected SELECT")
27279        };
27280        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27281            panic!()
27282        };
27283        assert_eq!(
27284            expr,
27285            &Expr::Column(ColumnName {
27286                qualifier: Some("t".into()),
27287                name: "col".into()
27288            })
27289        );
27290    }
27291
27292    #[test]
27293    fn select_item_alias_with_as() {
27294        let s = parse("SELECT a AS y FROM t");
27295        let Statement::Select(s) = s else {
27296            panic!("expected SELECT")
27297        };
27298        let SelectItem::Expr { alias, .. } = &s.items[0] else {
27299            panic!()
27300        };
27301        assert_eq!(alias.as_deref(), Some("y"));
27302    }
27303
27304    #[test]
27305    fn trailing_semicolon_accepted() {
27306        let s = parse("SELECT 1;");
27307        let Statement::Select(s) = s else {
27308            panic!("expected SELECT")
27309        };
27310        assert_eq!(s.items.len(), 1);
27311    }
27312
27313    #[test]
27314    fn boolean_chain_with_and_or_not() {
27315        // (NOT a) OR (b AND (NOT c))
27316        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27317        let Statement::Select(s) = s else {
27318            panic!("expected SELECT")
27319        };
27320        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27321            panic!()
27322        };
27323        let expected = Expr::Binary {
27324            lhs: Box::new(Expr::Unary {
27325                op: UnOp::Not,
27326                expr: Box::new(col("a")),
27327            }),
27328            op: BinOp::Or,
27329            rhs: Box::new(Expr::Binary {
27330                lhs: Box::new(col("b")),
27331                op: BinOp::And,
27332                rhs: Box::new(Expr::Unary {
27333                    op: UnOp::Not,
27334                    expr: Box::new(col("c")),
27335                }),
27336            }),
27337        };
27338        assert_eq!(expr, &expected);
27339    }
27340
27341    #[test]
27342    fn empty_input_errors() {
27343        // v7.14.0 — pg_dump preambles emit several comment-only
27344        // / blank-line statements that collapse to Statement::
27345        // Empty rather than a parse error. The old "SELECT in
27346        // message" assertion is stale; verify the new contract:
27347        // empty / whitespace / comment-only input parses to
27348        // Statement::Empty.
27349        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27350        assert!(matches!(
27351            parse_statement("  \n\t ").unwrap(),
27352            Statement::Empty
27353        ));
27354        // Sanity: malformed-but-non-empty still errors.
27355        assert!(parse_statement("SELECT FROM WHERE").is_err());
27356    }
27357
27358    #[test]
27359    fn unmatched_paren_errors() {
27360        assert!(parse_statement("SELECT (1 + 2").is_err());
27361    }
27362
27363    #[test]
27364    fn display_round_trip_simple_select() {
27365        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27366        let text = original.to_string();
27367        let again = parse_statement(&text).expect("re-parse");
27368        assert_eq!(original, again);
27369    }
27370
27371    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27372
27373    #[test]
27374    fn create_table_single_column() {
27375        let s = parse("CREATE TABLE foo (a INT)");
27376        let Statement::CreateTable(c) = s else {
27377            panic!("expected CreateTable")
27378        };
27379        assert_eq!(c.name, "foo");
27380        assert_eq!(c.columns.len(), 1);
27381        assert_eq!(c.columns[0].name, "a");
27382        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27383        assert!(c.columns[0].nullable);
27384    }
27385
27386    #[test]
27387    fn create_table_multi_column_with_not_null_mix() {
27388        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27389        let Statement::CreateTable(c) = s else {
27390            panic!()
27391        };
27392        assert_eq!(c.columns.len(), 4);
27393        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27394        assert!(!c.columns[0].nullable);
27395        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27396        assert!(c.columns[1].nullable);
27397        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27398        assert!(!c.columns[2].nullable);
27399        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27400    }
27401
27402    #[test]
27403    fn create_table_bigint_supported() {
27404        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27405        let Statement::CreateTable(c) = s else {
27406            panic!()
27407        };
27408        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27409    }
27410
27411    #[test]
27412    fn create_table_vector_default_is_f32() {
27413        let s = parse("CREATE TABLE t (v VECTOR(128))");
27414        let Statement::CreateTable(c) = s else {
27415            panic!()
27416        };
27417        assert_eq!(
27418            c.columns[0].ty,
27419            ColumnTypeName::Vector {
27420                dim: 128,
27421                encoding: VecEncoding::F32,
27422            },
27423        );
27424    }
27425
27426    #[test]
27427    fn create_table_vector_using_sq8() {
27428        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27429        // Case-insensitive on both `USING` and the encoding name.
27430        for sql in [
27431            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27432            "CREATE TABLE t (v VECTOR(128) using sq8)",
27433        ] {
27434            let s = parse(sql);
27435            let Statement::CreateTable(c) = s else {
27436                panic!()
27437            };
27438            assert_eq!(
27439                c.columns[0].ty,
27440                ColumnTypeName::Vector {
27441                    dim: 128,
27442                    encoding: VecEncoding::Sq8,
27443                },
27444                "{sql}",
27445            );
27446        }
27447    }
27448
27449    #[test]
27450    fn create_table_vector_using_unknown_errors() {
27451        // v7.16.1 — the inline `USING <encoding>` shape on
27452        // CREATE TABLE column defs was withdrawn before
27453        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27454        // (col vector_<metric>_ops)`; the parser now rejects
27455        // USING at column-list position with a clearer
27456        // "expected ',' or ')'" message. Test asserts the
27457        // current rejection, not the old "unknown vector
27458        // encoding" string.
27459        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27460        assert!(
27461            err.message.contains("USING")
27462                || err.message.contains("using")
27463                || err.message.contains("')'")
27464                || err.message.contains("','"),
27465            "expected USING/column-list rejection, got: {}",
27466            err.message
27467        );
27468    }
27469
27470    #[test]
27471    fn vector_using_sq8_display_roundtrips() {
27472        // The Display impl must produce text that re-parses to the
27473        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27474        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27475        let Statement::CreateTable(c) = s else {
27476            panic!()
27477        };
27478        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27479    }
27480
27481    #[test]
27482    fn parser_recognises_placeholders() {
27483        use crate::ast::{Expr, SelectItem, Statement};
27484        // $N in expression position parses as Expr::Placeholder(N).
27485        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27486        let Statement::Select(sel) = s else { panic!() };
27487        assert!(matches!(
27488            sel.items[0],
27489            SelectItem::Expr {
27490                expr: Expr::Placeholder(1),
27491                alias: None
27492            }
27493        ));
27494        // $2 + 1
27495        let SelectItem::Expr {
27496            expr: Expr::Binary { lhs, rhs, .. },
27497            ..
27498        } = &sel.items[1]
27499        else {
27500            panic!()
27501        };
27502        assert!(matches!(**lhs, Expr::Placeholder(2)));
27503        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27504        // WHERE x = $3
27505        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27506            panic!()
27507        };
27508        assert!(matches!(**rhs, Expr::Placeholder(3)));
27509    }
27510
27511    #[test]
27512    fn parser_rejects_dollar_zero() {
27513        // $0 is not valid in PG; the lexer rejects it.
27514        assert!(parse_statement("SELECT $0").is_err());
27515    }
27516
27517    #[test]
27518    fn placeholder_display_roundtrips() {
27519        // The Display impl must produce text that re-lexes to the
27520        // same Placeholder token.
27521        let s = parse("SELECT $42 FROM t");
27522        let printed = s.to_string();
27523        assert!(printed.contains("$42"));
27524        let again = parse(&printed);
27525        assert_eq!(s, again);
27526    }
27527
27528    #[test]
27529    fn alter_index_rebuild_bare() {
27530        use crate::ast::{AlterIndexTarget, Statement};
27531        let s = parse("ALTER INDEX my_idx REBUILD");
27532        let Statement::AlterIndex(a) = s else {
27533            panic!("expected AlterIndex, got {s:?}")
27534        };
27535        assert_eq!(a.name, "my_idx");
27536        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27537    }
27538
27539    #[test]
27540    fn alter_index_rebuild_with_encoding() {
27541        use crate::ast::{AlterIndexTarget, Statement};
27542        for (sql, want) in [
27543            (
27544                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27545                VecEncoding::F32,
27546            ),
27547            (
27548                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27549                VecEncoding::Sq8,
27550            ),
27551            (
27552                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27553                VecEncoding::F16,
27554            ),
27555        ] {
27556            let s = parse(sql);
27557            let Statement::AlterIndex(a) = s else {
27558                panic!("{sql}: expected AlterIndex")
27559            };
27560            assert_eq!(a.name, "my_idx");
27561            assert_eq!(
27562                a.target,
27563                AlterIndexTarget::Rebuild {
27564                    encoding: Some(want)
27565                },
27566                "{sql}"
27567            );
27568        }
27569    }
27570
27571    #[test]
27572    fn alter_index_rebuild_unknown_encoding_errors() {
27573        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
27574        assert!(
27575            err.message.contains("unknown vector encoding"),
27576            "got: {}",
27577            err.message
27578        );
27579    }
27580
27581    #[test]
27582    fn alter_index_rebuild_display_roundtrips() {
27583        for (input, want) in [
27584            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
27585            (
27586                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27587                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27588            ),
27589            (
27590                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27591                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27592            ),
27593        ] {
27594            let s = parse(input);
27595            assert_eq!(s.to_string(), want);
27596        }
27597    }
27598
27599    #[test]
27600    fn create_table_unknown_type_defers_to_engine() {
27601        // v4.9 picked XML as a parse-time "unsupported column
27602        // type" probe. v7.17.0 Phase 1.4 changed the contract:
27603        // an unknown type ident parses as Text + `user_type_ref`
27604        // so CREATE TABLE can resolve user-defined enum / domain
27605        // types — rejection of truly-unknown types moved to the
27606        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
27607        // to a first-class built-in, so this probe switched to a
27608        // synthetic name nothing in the lexer will ever recognise.
27609        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
27610        let Statement::CreateTable(t) = stmt else {
27611            panic!("expected CreateTable");
27612        };
27613        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
27614    }
27615
27616    #[test]
27617    fn create_table_missing_table_keyword_errors() {
27618        assert!(parse_statement("CREATE x (a INT)").is_err());
27619    }
27620
27621    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
27622    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
27623
27624    #[test]
27625    fn parse_create_table_partition_by_range() {
27626        use crate::ast::{PartitionBySpec, PartitionKindAst};
27627        let stmt = parse_statement(
27628            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
27629             payload JSONB) PARTITION BY RANGE (ts)",
27630        )
27631        .unwrap();
27632        let Statement::CreateTable(t) = stmt else {
27633            panic!("expected CreateTable");
27634        };
27635        assert!(t.partition_of.is_none(), "parent has no partition_of");
27636        assert_eq!(t.columns.len(), 3);
27637        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
27638        assert_eq!(
27639            by,
27640            &PartitionBySpec {
27641                kind: PartitionKindAst::Range,
27642                key_columns: alloc::vec!["ts".to_string()],
27643            }
27644        );
27645        // Display round-trip preserves the suffix. `quote_ident`
27646        // only adds double quotes when the ident needs escaping, so
27647        // a plain `ts` survives bare here.
27648        assert!(
27649            t.to_string().contains("PARTITION BY RANGE (ts)"),
27650            "Display lost PARTITION BY suffix: {t}"
27651        );
27652    }
27653
27654    #[test]
27655    fn parse_create_table_partition_of_range() {
27656        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
27657        let stmt = parse_statement(
27658            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
27659             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
27660        )
27661        .unwrap();
27662        let Statement::CreateTable(t) = stmt else {
27663            panic!("expected CreateTable");
27664        };
27665        assert!(t.columns.is_empty(), "child inherits columns from parent");
27666        assert!(t.partition_by.is_none());
27667        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27668        assert_eq!(of.parent_name, "events_partitioned");
27669        let PartitionOfSpec { bounds, .. } = of.clone();
27670        match bounds {
27671            PartitionOfBoundsAst::Range { lower, upper } => {
27672                assert!(lower.to_string().contains("2026-06-01"));
27673                assert!(upper.to_string().contains("2026-07-01"));
27674            }
27675            other => panic!("expected Range, got {other:?}"),
27676        }
27677        // Display round-trip emits the FOR VALUES tail. `quote_ident`
27678        // skips quotes when not required, so the parent name appears
27679        // bare here.
27680        let s = t.to_string();
27681        assert!(
27682            s.contains("PARTITION OF events_partitioned"),
27683            "Display lost PARTITION OF: {s}"
27684        );
27685        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
27686        assert!(s.contains(") TO ("), "Display lost TO: {s}");
27687    }
27688
27689    #[test]
27690    fn parse_create_table_partition_of_default() {
27691        use crate::ast::PartitionOfBoundsAst;
27692        let stmt =
27693            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
27694                .unwrap();
27695        let Statement::CreateTable(t) = stmt else {
27696            panic!("expected CreateTable");
27697        };
27698        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27699        assert_eq!(of.parent_name, "events_partitioned");
27700        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
27701        assert!(
27702            t.to_string()
27703                .contains("PARTITION OF events_partitioned DEFAULT"),
27704            "Display lost DEFAULT: {t}"
27705        );
27706    }
27707
27708    #[test]
27709    fn parse_create_table_partition_by_list() {
27710        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
27711        // child with `FOR VALUES IN (lit, lit, …)`.
27712        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27713        let parent =
27714            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
27715                .unwrap();
27716        let Statement::CreateTable(t) = parent else {
27717            panic!("expected CreateTable");
27718        };
27719        let Some(PartitionBySpec {
27720            kind,
27721            ref key_columns,
27722        }) = t.partition_by
27723        else {
27724            panic!("expected PARTITION BY");
27725        };
27726        assert_eq!(kind, PartitionKindAst::List);
27727        assert_eq!(*key_columns, vec!["region".to_string()]);
27728        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
27729
27730        let child = parse_statement(
27731            "CREATE TABLE events_apac PARTITION OF events_listed \
27732             FOR VALUES IN ('jp', 'kr', 'tw')",
27733        )
27734        .unwrap();
27735        let Statement::CreateTable(c) = child else {
27736            panic!("expected CreateTable");
27737        };
27738        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27739        let PartitionOfBoundsAst::List { values } = &of.bounds else {
27740            panic!("expected List bounds, got {:?}", of.bounds);
27741        };
27742        assert_eq!(values.len(), 3);
27743        let disp = c.to_string();
27744        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
27745    }
27746
27747    #[test]
27748    fn parse_create_table_partition_by_hash() {
27749        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
27750        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
27751        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27752        let parent =
27753            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
27754        let Statement::CreateTable(t) = parent else {
27755            panic!("expected CreateTable");
27756        };
27757        let Some(PartitionBySpec {
27758            kind,
27759            ref key_columns,
27760        }) = t.partition_by
27761        else {
27762            panic!("expected PARTITION BY");
27763        };
27764        assert_eq!(kind, PartitionKindAst::Hash);
27765        assert_eq!(*key_columns, vec!["id".to_string()]);
27766        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
27767
27768        let child = parse_statement(
27769            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
27770             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
27771        )
27772        .unwrap();
27773        let Statement::CreateTable(c) = child else {
27774            panic!("expected CreateTable");
27775        };
27776        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27777        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
27778            panic!("expected Hash bounds");
27779        };
27780        assert_eq!(modulus, 4);
27781        assert_eq!(remainder, 0);
27782        let disp = c.to_string();
27783        assert!(
27784            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
27785            "Display lost HASH bounds: {disp}"
27786        );
27787
27788        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
27789        let bad = parse_statement(
27790            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
27791             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
27792        );
27793        let msg = format!("{}", bad.unwrap_err());
27794        assert!(
27795            msg.contains("REMAINDER") && msg.contains("MODULUS"),
27796            "expected REMAINDER/MODULUS validation error: {msg}"
27797        );
27798    }
27799
27800    #[test]
27801    fn parse_create_table_partition_of_rejects_columns() {
27802        // v7.37.6-B contract: PARTITION OF children inherit columns
27803        // from the parent; an explicit list MUST surface as a parse
27804        // error rather than getting silently ignored.
27805        let err = parse_statement(
27806            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
27807             FOR VALUES FROM ('a') TO ('b')",
27808        );
27809        assert!(err.is_err(), "expected parse error for explicit columns");
27810        let msg = format!("{}", err.unwrap_err());
27811        assert!(
27812            msg.contains("PARTITION OF") && msg.contains("column"),
27813            "error should mention PARTITION OF + columns: {msg}"
27814        );
27815    }
27816
27817    #[test]
27818    fn insert_single_value() {
27819        let s = parse("INSERT INTO foo VALUES (42)");
27820        let Statement::Insert(i) = s else {
27821            panic!("expected Insert")
27822        };
27823        assert_eq!(i.table, "foo");
27824        assert_eq!(i.rows.len(), 1);
27825        assert_eq!(i.rows[0].len(), 1);
27826        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
27827    }
27828
27829    #[test]
27830    fn insert_multi_value_with_mixed_literals() {
27831        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
27832        let Statement::Insert(i) = s else { panic!() };
27833        assert_eq!(i.rows.len(), 1);
27834        assert_eq!(i.rows[0].len(), 5);
27835    }
27836
27837    #[test]
27838    fn insert_missing_into_errors() {
27839        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
27840    }
27841
27842    #[test]
27843    fn create_table_round_trip() {
27844        let original =
27845            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
27846        let text = original.to_string();
27847        let again = parse_statement(&text).expect("re-parse");
27848        assert_eq!(original, again);
27849    }
27850
27851    #[test]
27852    fn insert_round_trip_with_negation_and_string() {
27853        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
27854        let text = original.to_string();
27855        let again = parse_statement(&text).expect("re-parse");
27856        assert_eq!(original, again);
27857    }
27858
27859    #[test]
27860    fn unknown_keyword_at_statement_start_errors() {
27861        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
27862        // the top-level dispatch still has no branch to take.
27863        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
27864        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
27865    }
27866
27867    // --- v0.8 CREATE INDEX --------------------------------------------------
27868
27869    #[test]
27870    fn create_index_basic() {
27871        let s = parse("CREATE INDEX idx_id ON users (id)");
27872        let Statement::CreateIndex(c) = s else {
27873            panic!("expected CreateIndex")
27874        };
27875        assert_eq!(c.name, "idx_id");
27876        assert_eq!(c.table, "users");
27877        assert_eq!(c.column, "id");
27878    }
27879
27880    #[test]
27881    fn create_index_missing_on_errors() {
27882        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
27883    }
27884
27885    #[test]
27886    fn create_index_missing_paren_errors() {
27887        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
27888    }
27889
27890    #[test]
27891    fn create_index_round_trip() {
27892        let original = parse("CREATE INDEX by_name ON users (name)");
27893        let again = parse_statement(&original.to_string()).unwrap();
27894        assert_eq!(original, again);
27895    }
27896
27897    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
27898
27899    #[test]
27900    fn create_unique_index_basic() {
27901        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
27902        let Statement::CreateIndex(c) = s else {
27903            panic!("expected CreateIndex");
27904        };
27905        assert!(c.is_unique);
27906        assert_eq!(c.column, "a");
27907        assert!(c.partial_predicate.is_none());
27908    }
27909
27910    #[test]
27911    fn create_unique_index_partial() {
27912        // mailrs's email_templates "one default per user" shape.
27913        let s = parse(
27914            "CREATE UNIQUE INDEX idx_email_templates_user_default \
27915             ON email_templates (user_address) WHERE is_default = true",
27916        );
27917        let Statement::CreateIndex(c) = s else {
27918            panic!("expected CreateIndex");
27919        };
27920        assert!(c.is_unique);
27921        assert_eq!(c.table, "email_templates");
27922        assert_eq!(c.column, "user_address");
27923        assert!(c.partial_predicate.is_some());
27924    }
27925
27926    #[test]
27927    fn create_unique_index_composite_with_predicate() {
27928        // mailrs's calendar_events instance: composite columns.
27929        let s = parse(
27930            "CREATE UNIQUE INDEX uq_calendar_events_instance \
27931             ON calendar_events (calendar_id, uid, recurrence_id) \
27932             WHERE recurrence_id IS NOT NULL",
27933        );
27934        let Statement::CreateIndex(c) = s else {
27935            panic!("expected CreateIndex");
27936        };
27937        assert!(c.is_unique);
27938        assert_eq!(c.column, "calendar_id");
27939        assert_eq!(
27940            c.extra_columns,
27941            vec!["uid".to_string(), "recurrence_id".to_string()]
27942        );
27943        assert!(c.partial_predicate.is_some());
27944    }
27945
27946    #[test]
27947    fn create_unique_index_using_btree_ok() {
27948        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
27949        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
27950    }
27951
27952    #[test]
27953    fn create_unique_index_using_hnsw_rejected() {
27954        let err =
27955            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
27956        assert!(err.message.contains("UNIQUE"), "{}", err.message);
27957    }
27958
27959    #[test]
27960    fn create_unique_index_round_trip() {
27961        let original = parse(
27962            "CREATE UNIQUE INDEX uq_calendar_events_master \
27963             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
27964        );
27965        let again = parse_statement(&original.to_string()).unwrap();
27966        assert_eq!(original, again);
27967    }
27968
27969    #[test]
27970    fn create_unique_without_index_errors() {
27971        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
27972        // v7.39 (round 340, V56) — PG 18.4, verbatim.
27973        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
27974    }
27975
27976    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
27977
27978    #[test]
27979    fn create_table_bytea_column() {
27980        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
27981        let Statement::CreateTable(c) = s else {
27982            panic!("expected CreateTable");
27983        };
27984        assert_eq!(c.columns.len(), 2);
27985        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
27986        assert!(!c.columns[1].nullable);
27987    }
27988
27989    #[test]
27990    fn create_table_bytes_alias_column() {
27991        let s = parse("CREATE TABLE t (blob BYTES)");
27992        let Statement::CreateTable(c) = s else {
27993            panic!("expected CreateTable");
27994        };
27995        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
27996    }
27997
27998    #[test]
27999    fn bytea_round_trip_display() {
28000        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28001        let again = parse_statement(&original.to_string()).unwrap();
28002        assert_eq!(original, again);
28003    }
28004
28005    // --- v0.9 transactions -------------------------------------------------
28006
28007    #[test]
28008    fn begin_commit_rollback_parse_as_unit_variants() {
28009        assert_eq!(parse("BEGIN"), Statement::Begin(None));
28010        assert_eq!(parse("COMMIT"), Statement::Commit);
28011        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28012        // Trailing semicolons accepted too.
28013        assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28014        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28015        // statement (with or without the WORK/TRANSACTION noise word).
28016        assert_eq!(
28017            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28018            Statement::Begin(Some(IsolationLevel::RepeatableRead))
28019        );
28020        assert_eq!(
28021            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28022            Statement::Begin(Some(IsolationLevel::Serializable))
28023        );
28024        // A non-isolation mode keeps the session default (None).
28025        assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28026    }
28027
28028    // --- v1.2: pgvector distance ops + ::vector cast --------------------
28029
28030    #[test]
28031    fn inner_product_binop_parses() {
28032        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28033        let Statement::Select(s) = s else { panic!() };
28034        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28035            panic!()
28036        };
28037        assert!(matches!(
28038            expr,
28039            Expr::Binary {
28040                op: BinOp::InnerProduct,
28041                ..
28042            }
28043        ));
28044    }
28045
28046    #[test]
28047    fn cosine_distance_binop_parses() {
28048        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28049        let Statement::Select(s) = s else { panic!() };
28050        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28051            panic!()
28052        };
28053        assert!(matches!(
28054            expr,
28055            Expr::Binary {
28056                op: BinOp::CosineDistance,
28057                ..
28058            }
28059        ));
28060    }
28061
28062    #[test]
28063    fn vector_cast_postfix_wraps_string_literal() {
28064        let s = parse("SELECT '[1,2,3]'::vector FROM t");
28065        let Statement::Select(s) = s else { panic!() };
28066        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28067            panic!()
28068        };
28069        assert!(matches!(
28070            expr,
28071            Expr::Cast {
28072                target: CastTarget::Vector,
28073                ..
28074            }
28075        ));
28076    }
28077
28078    #[test]
28079    fn unsupported_cast_target_errors() {
28080        // v7.37.5 ship triage promoted the parser to accept every
28081        // ident as a `CastTarget::Named(canonical)`; the engine
28082        // surfaces the "unsupported cast target" error at eval
28083        // time when `type_name_to_data_type` can't resolve it.
28084        // Parser-side error now requires a NON-ident after `::`
28085        // (e.g. a punctuation token).
28086        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28087        assert_eq!(err.message, "syntax error at or near \",\"");
28088    }
28089
28090    #[test]
28091    fn tx_statements_round_trip() {
28092        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28093            let original = parse(q);
28094            let again = parse_statement(&original.to_string()).unwrap();
28095            assert_eq!(original, again);
28096        }
28097    }
28098
28099    #[test]
28100    fn interval_text_parsing_units() {
28101        // v7.37.5 β — three-field shape `(months, days, micros)` so
28102        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28103        // Single unit.
28104        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28105        assert_eq!(
28106            parse_interval_text("24 hours"),
28107            Some((0, 0, 86_400_000_000))
28108        );
28109        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28110        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28111        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28112        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28113        // Compound spans accumulate per-dimension.
28114        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28115        assert_eq!(
28116            parse_interval_text("1 day 2 hours"),
28117            Some((0, 1, 7_200_000_000))
28118        );
28119        // Negative numbers carry through per-dimension.
28120        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28121        // Bad shapes return None.
28122        assert_eq!(parse_interval_text(""), None);
28123        assert_eq!(parse_interval_text("garbage"), None);
28124        assert_eq!(parse_interval_text("1 fortnight"), None);
28125        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28126        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28127        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28128        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28129        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28130    }
28131
28132    #[test]
28133    fn interval_literal_roundtrips_via_display() {
28134        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28135        let s = parsed.to_string();
28136        // Display preserves the original text verbatim.
28137        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28138        // And re-parsing yields a structurally equal statement.
28139        let again = parse_statement(&s).unwrap();
28140        assert_eq!(parsed, again);
28141    }
28142
28143    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28144
28145    #[test]
28146    fn parser_recognises_create_publication_bare() {
28147        let s = parse("CREATE PUBLICATION pub_a");
28148        let Statement::CreatePublication(p) = s else {
28149            panic!("expected CreatePublication, got {s:?}")
28150        };
28151        assert_eq!(p.name, "pub_a");
28152        assert_eq!(p.scope, PublicationScope::AllTables);
28153    }
28154
28155    #[test]
28156    fn parser_recognises_create_publication_for_all_tables() {
28157        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28158        let Statement::CreatePublication(p) = s else {
28159            panic!("expected CreatePublication, got {s:?}")
28160        };
28161        assert_eq!(p.name, "pub_a");
28162        assert_eq!(p.scope, PublicationScope::AllTables);
28163    }
28164
28165    #[test]
28166    fn parser_recognises_drop_publication() {
28167        let s = parse("DROP PUBLICATION pub_a");
28168        let Statement::DropPublication { name, .. } = s else {
28169            panic!("expected DropPublication, got {s:?}")
28170        };
28171        assert_eq!(name, "pub_a");
28172    }
28173
28174    #[test]
28175    fn parser_recognises_for_table_list() {
28176        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28177        let Statement::CreatePublication(p) = s else {
28178            panic!("expected CreatePublication, got {s:?}")
28179        };
28180        assert_eq!(p.name, "pub_a");
28181        let PublicationScope::ForTables(ts) = p.scope else {
28182            panic!("expected ForTables scope")
28183        };
28184        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28185    }
28186
28187    #[test]
28188    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28189        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28190        // is rejected (`invalid publication object list`; the old
28191        // test pinned an unverifiable "PG 19 accepts both" claim);
28192        // TABLES pairs with IN SCHEMA.
28193        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28194            .expect_err("bare FOR TABLES must reject");
28195        assert!(
28196            alloc::format!("{err}").contains("invalid publication object list"),
28197            "got: {err}"
28198        );
28199        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28200        let Statement::CreatePublication(p) = s else {
28201            panic!("expected CreatePublication, got {s:?}")
28202        };
28203        let PublicationScope::TablesInSchema(schema) = p.scope else {
28204            panic!("expected TablesInSchema")
28205        };
28206        assert_eq!(schema, "public");
28207    }
28208
28209    #[test]
28210    fn parser_recognises_for_all_tables_except_list() {
28211        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28212        let Statement::CreatePublication(p) = s else {
28213            panic!()
28214        };
28215        let PublicationScope::AllTablesExcept(ts) = p.scope else {
28216            panic!("expected AllTablesExcept")
28217        };
28218        assert_eq!(ts, alloc::vec!["t1", "t2"]);
28219    }
28220
28221    #[test]
28222    fn parser_rejects_for_table_with_empty_list() {
28223        // `FOR TABLE` with nothing after is a parse error.
28224        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28225            .expect_err("must error on empty list");
28226        // No specific message asserted — the call falls through to
28227        // expect_ident_like which yields "expected identifier, got …".
28228        assert!(!err.message.is_empty());
28229    }
28230
28231    #[test]
28232    fn parser_recognises_show_publications() {
28233        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28234        // bare ident in this position, NOT a reserved keyword.
28235        let s = parse("SHOW PUBLICATIONS");
28236        assert!(matches!(s, Statement::ShowPublications));
28237    }
28238
28239    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28240
28241    #[test]
28242    fn parser_recognises_create_subscription_single_publication() {
28243        let s = parse(
28244            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28245        );
28246        let Statement::CreateSubscription(c) = s else {
28247            panic!("expected CreateSubscription, got {s:?}")
28248        };
28249        assert_eq!(c.name, "sub_a");
28250        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28251        assert_eq!(c.publications, alloc::vec!["pub_a"]);
28252    }
28253
28254    #[test]
28255    fn parser_recognises_create_subscription_multi_publication() {
28256        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28257        let Statement::CreateSubscription(c) = s else {
28258            panic!()
28259        };
28260        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28261    }
28262
28263    #[test]
28264    fn parser_rejects_create_subscription_missing_connection() {
28265        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28266            .expect_err("must error on missing CONNECTION");
28267        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28268    }
28269
28270    #[test]
28271    fn parser_rejects_create_subscription_missing_publication() {
28272        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28273            .expect_err("must error on missing PUBLICATION");
28274        assert_eq!(err.message, "syntax error at end of input");
28275    }
28276
28277    #[test]
28278    fn parser_recognises_drop_subscription() {
28279        let s = parse("DROP SUBSCRIPTION sub_a");
28280        let Statement::DropSubscription { name, .. } = s else {
28281            panic!("expected DropSubscription, got {s:?}")
28282        };
28283        assert_eq!(name, "sub_a");
28284    }
28285
28286    #[test]
28287    fn parser_recognises_show_subscriptions() {
28288        let s = parse("SHOW SUBSCRIPTIONS");
28289        assert!(matches!(s, Statement::ShowSubscriptions));
28290    }
28291
28292    #[test]
28293    fn parser_recognises_wait_for_wal_position_no_timeout() {
28294        let s = parse("WAIT FOR WAL POSITION 12345");
28295        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28296            panic!("expected WaitForWalPosition, got {s:?}")
28297        };
28298        assert_eq!(pos, 12345);
28299        assert!(timeout_ms.is_none());
28300    }
28301
28302    #[test]
28303    fn parser_recognises_wait_for_wal_position_with_timeout() {
28304        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28305        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28306            panic!()
28307        };
28308        assert_eq!(pos, 67890);
28309        assert_eq!(timeout_ms, Some(5000));
28310    }
28311
28312    #[test]
28313    fn parser_rejects_wait_with_negative_position() {
28314        // The lexer treats `-` as a token; `expect_u64_literal`
28315        // only sees the Integer that follows, so the negative
28316        // arrives as a unary-minus expression at higher levels.
28317        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28318        // parse error one way or another.
28319        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28320        assert!(!err.message.is_empty());
28321    }
28322
28323    #[test]
28324    fn parser_recognises_bare_analyze() {
28325        let s = parse("ANALYZE");
28326        assert!(matches!(s, Statement::Analyze(None)));
28327    }
28328
28329    #[test]
28330    fn parser_recognises_analyze_with_table() {
28331        let s = parse("ANALYZE users");
28332        let Statement::Analyze(Some(name)) = s else {
28333            panic!("expected Analyze, got {s:?}")
28334        };
28335        assert_eq!(name, "users");
28336    }
28337
28338    #[test]
28339    fn parser_recognises_analyze_with_quoted_table() {
28340        let s = parse("ANALYZE \"Mixed Case\"");
28341        let Statement::Analyze(Some(name)) = s else {
28342            panic!()
28343        };
28344        assert_eq!(name, "Mixed Case");
28345    }
28346
28347    #[test]
28348    fn parser_rejects_analyze_with_garbage_token() {
28349        let err = parse_statement("ANALYZE 42").expect_err("must error");
28350        assert!(!err.message.is_empty());
28351    }
28352
28353    #[test]
28354    fn analyze_display_roundtrips() {
28355        for sql in ["ANALYZE", "ANALYZE users"] {
28356            let s = parse(sql);
28357            let printed = s.to_string();
28358            let again = parse_statement(&printed)
28359                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28360            assert_eq!(s, again);
28361        }
28362    }
28363
28364    #[test]
28365    fn wait_for_display_roundtrips() {
28366        for sql in [
28367            "WAIT FOR WAL POSITION 12345",
28368            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28369        ] {
28370            let s = parse(sql);
28371            let printed = s.to_string();
28372            let again = parse_statement(&printed)
28373                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28374            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28375        }
28376    }
28377
28378    #[test]
28379    fn subscription_ddl_display_roundtrips() {
28380        for sql in [
28381            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28382            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28383            "DROP SUBSCRIPTION sub_a",
28384            "SHOW SUBSCRIPTIONS",
28385        ] {
28386            let s = parse(sql);
28387            let printed = s.to_string();
28388            let again = parse_statement(&printed)
28389                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28390            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28391        }
28392    }
28393
28394    #[test]
28395    fn parser_drop_dispatches_user_vs_publication() {
28396        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28397        // tokenises DROP. Both targets must still parse.
28398        let s = parse("DROP USER 'alice'");
28399        let Statement::DropUser { name, .. } = s else {
28400            panic!("expected DropUser, got {s:?}")
28401        };
28402        assert_eq!(name, "alice");
28403        // And DROP PUBLICATION lands the new variant.
28404        let s = parse("DROP PUBLICATION p1");
28405        assert!(matches!(s, Statement::DropPublication { .. }));
28406    }
28407
28408    #[test]
28409    fn publication_ddl_display_roundtrips() {
28410        // Every CREATE PUBLICATION variant must Display → parse →
28411        // same AST. v6.1.3 covers all three scope shapes.
28412        for sql in [
28413            "CREATE PUBLICATION pub_a",
28414            "CREATE PUBLICATION pub_a FOR ALL TABLES",
28415            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28416            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28417            "DROP PUBLICATION pub_a",
28418            "SHOW PUBLICATIONS",
28419        ] {
28420            let s = parse(sql);
28421            let printed = s.to_string();
28422            let again = parse_statement(&printed)
28423                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28424            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28425        }
28426    }
28427
28428    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28429
28430    #[test]
28431    fn create_function_returns_trigger_plpgsql_minimal() {
28432        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28433        let s = parse(sql);
28434        let Statement::CreateFunction(f) = s else {
28435            panic!("expected CreateFunction");
28436        };
28437        assert_eq!(f.name, "noop");
28438        assert!(!f.or_replace);
28439        assert!(f.args.is_empty());
28440        assert!(matches!(f.returns, FunctionReturn::Trigger));
28441        assert_eq!(f.language, "plpgsql");
28442        let FunctionBody::PlPgSql(block) = f.body else {
28443            panic!("expected PlPgSql body");
28444        };
28445        assert_eq!(block.statements.len(), 1);
28446        assert!(matches!(
28447            block.statements[0],
28448            PlPgSqlStmt::Return(ReturnTarget::New)
28449        ));
28450    }
28451
28452    #[test]
28453    fn create_function_or_replace_with_assignment() {
28454        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28455        // RETURN NEW.
28456        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28457BEGIN
28458  NEW.search_vector := to_tsvector('english', NEW.subject);
28459  RETURN NEW;
28460END;
28461$$";
28462        let s = parse(sql);
28463        let Statement::CreateFunction(f) = s else {
28464            panic!("expected CreateFunction");
28465        };
28466        assert!(f.or_replace);
28467        let FunctionBody::PlPgSql(block) = &f.body else {
28468            panic!("expected PlPgSql body");
28469        };
28470        assert_eq!(block.statements.len(), 2);
28471        // First statement: NEW.search_vector := to_tsvector(...)
28472        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28473            panic!("expected Assign as first stmt");
28474        };
28475        match target {
28476            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28477            other => panic!("expected NEW.col, got {other:?}"),
28478        }
28479        // Second statement: RETURN NEW
28480        assert!(matches!(
28481            block.statements[1],
28482            PlPgSqlStmt::Return(ReturnTarget::New)
28483        ));
28484    }
28485
28486    #[test]
28487    fn create_trigger_after_insert_or_update() {
28488        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28489        let s = parse(sql);
28490        let Statement::CreateTrigger(t) = s else {
28491            panic!("expected CreateTrigger");
28492        };
28493        assert_eq!(t.name, "tg");
28494        assert_eq!(t.table, "messages");
28495        assert_eq!(t.timing, TriggerTiming::After);
28496        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28497        assert_eq!(t.for_each, TriggerForEach::Row);
28498        assert_eq!(t.function, "update_sv");
28499    }
28500
28501    #[test]
28502    fn create_trigger_before_delete_execute_procedure_alias() {
28503        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28504        let sql =
28505            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28506        let s = parse(sql);
28507        let Statement::CreateTrigger(t) = s else {
28508            panic!("expected CreateTrigger");
28509        };
28510        assert_eq!(t.timing, TriggerTiming::Before);
28511        assert_eq!(t.events, vec![TriggerEvent::Delete]);
28512    }
28513
28514    #[test]
28515    fn drop_trigger_if_exists_round_trips() {
28516        // No parser support for DROP TRIGGER yet — added in v7.12.5
28517        // alongside the broader DROP …{IF EXISTS} cleanup. The
28518        // AST + Display impls are in place so we round-trip via
28519        // construction:
28520        let s = Statement::DropTrigger {
28521            name: "tg".into(),
28522            table: "messages".into(),
28523            if_exists: true,
28524        };
28525        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28526    }
28527
28528    #[test]
28529    fn trigger_ddl_display_roundtrips_through_parser() {
28530        // CREATE TRIGGER + its referenced CREATE FUNCTION must
28531        // Display → parse → same AST (modulo PL/pgSQL body
28532        // formatting which is parser-canonicalised).
28533        for sql in [
28534            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28535            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28536        ] {
28537            let s = parse(sql);
28538            let printed = s.to_string();
28539            let again = parse_statement(&printed)
28540                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28541            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28542        }
28543    }
28544}