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
432impl Parser {
433    /// Whether what follows an identifier ends an index key, which is how
434    /// an operator class is told from anything else in that position.
435    fn opclass_position_follows(next: Option<&Token>) -> bool {
436        match next {
437            // `ASC` / `DESC` have their own tokens; matching them as
438            // identifiers named "asc" / "desc" — which the first version of
439            // this did — never fires, and `(c text_pattern_ops DESC)` (which
440            // PG18.4 accepts, verified) went on failing to parse.
441            Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
442            Some(Token::Ident(w)) => {
443                w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
444            }
445            _ => false,
446        }
447    }
448}
449
450fn is_vector_opclass_name(name: &str) -> bool {
451    let lc = name.to_ascii_lowercase();
452    matches!(
453        lc.as_str(),
454        "vector_cosine_ops"
455            | "vector_l2_ops"
456            | "vector_ip_ops"
457            | "halfvec_cosine_ops"
458            | "halfvec_l2_ops"
459            | "halfvec_ip_ops"
460            | "sq8_cosine_ops"
461            | "sq8_l2_ops"
462            | "sq8_ip_ops"
463            // pg_trgm — trigram operator class. SPG's GIN index
464            // already uses tsvector tokens; trigram-style LIKE
465            // pattern matching still routes through a sequential
466            // scan, but the opclass name is accepted so PG schemas
467            // load.
468            | "gin_trgm_ops"
469            | "gist_trgm_ops"
470            // PG built-in btree opclasses occasionally appear in
471            // pg_dump output for column types with multiple
472            // sort orders (text_pattern_ops, varchar_pattern_ops,
473            // bpchar_pattern_ops).
474            | "text_pattern_ops"
475            | "varchar_pattern_ops"
476            | "bpchar_pattern_ops"
477            | "int4_ops"
478            | "int8_ops"
479            | "text_ops"
480    )
481}
482
483#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct ParseError {
485    pub message: String,
486    /// Index into the token stream where parsing tripped. Not a byte offset.
487    /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
488    /// field would grow every `Result<_, ParseError>` slot on the deeply
489    /// recursive parse stack and tip the nesting-budget frame cliff. PG's
490    /// 1-based char position is recovered on the cold error path by
491    /// [`syntax_error_position`], which re-tokenizes to map this token index.
492    pub token_pos: usize,
493}
494
495impl fmt::Display for ParseError {
496    /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
497    /// with `parse error at token #N: `, which PG has no equivalent of:
498    /// the message bodies are already PG's verbatim (`LIMIT must not be
499    /// negative`, `invalid input syntax for type bigint: "abc"`), and the
500    /// prefix was SPG's internal token index leaking into every one of
501    /// them. `token_pos` stays a field — the wire recovers PG's 1-based
502    /// character position from it for the ErrorResponse `P`.
503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504        f.write_str(&self.message)
505    }
506}
507
508impl From<LexError> for ParseError {
509    fn from(e: LexError) -> Self {
510        Self {
511            message: format!("lex: {e}"),
512            token_pos: 0,
513        }
514    }
515}
516
517/// v7.9.30 — parse a single expression (no trailing junk). Used by
518/// the engine to re-hydrate stored partial-index / unique-index
519/// predicates from their canonical Display form. The same Pratt
520/// parser the statement path uses; this entry point just skips the
521/// statement dispatch.
522pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
523    let (tokens, offsets) =
524        lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
525    let mut p = Parser::new(tokens);
526    let expr = p
527        .parse_expr(0)
528        .and_then(|e| p.expect_eof().map(|()| e))
529        .map_err(|e| shape_syntax_error(e, input, &offsets))?;
530    Ok(expr)
531}
532
533/// Parse exactly one statement, swallow an optional trailing `;`, and require
534/// the token stream to end there. PG string semantics.
535pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
536    parse_statement_with(input, false)
537}
538
539/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
540/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
541/// The engine threads its session flag through here.
542pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
543    let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
544        .map_err(|e| shape_lex_error(&e, input))?;
545    // The same session flag names the dialect for both the lexer and
546    // the type mapping.
547    let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
548    let stmt = (|| {
549        let stmt = p.parse_one_statement()?;
550        if matches!(p.peek(), Token::Semicolon) {
551            p.advance();
552        }
553        p.expect_eof()?;
554        Ok(stmt)
555    })()
556    .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
557    Ok(stmt)
558}
559
560/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
561/// `syntax error at or near "<token>"` and `syntax error at end of input`
562/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
563/// prose — `expected identifier, got Eof`, `unexpected token From in
564/// expression`, `expected end of input, got Ident("with")` — which named
565/// internal token types and, in the Debug forms, leaked the parser's own
566/// enum into a message clients read.
567///
568/// Applied once on the way out, so every construction site is covered and
569/// the token named is the one the error itself points at. Messages whose
570/// bodies are already PG's verbatim (`LIMIT must not be negative`,
571/// `invalid input syntax for type bigint: "abc"`) are left alone — those
572/// are PG's own errors, not its syntax error.
573fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
574    if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
575        return e;
576    }
577    let message = match offending_lexeme(input, offsets, e.token_pos) {
578        Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
579        None => "syntax error at end of input".into(),
580    };
581    ParseError {
582        message,
583        token_pos: e.token_pos,
584    }
585}
586
587/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
588/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
589/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
590/// comment at or near "/* x"` — the quoted part runs from the opening
591/// delimiter to the end of the input. SPG reported its own internal
592/// shape instead (`unterminated string literal at byte 7`), which named
593/// a byte offset no client can use.
594fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
595    use lexer::LexErrorKind as K;
596    let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
597    let message = match &e.kind {
598        K::UnterminatedString => {
599            alloc::format!("unterminated quoted string at or near \"{from_here}\"")
600        }
601        K::UnterminatedQuotedIdent => {
602            alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
603        }
604        K::UnterminatedBlockComment => {
605            alloc::format!("unterminated /* comment at or near \"{from_here}\"")
606        }
607        // PG has no "unknown character" error of its own — the character
608        // is skipped and the parser reports the next token. SPG stops at
609        // the character itself and names it, which is the same shape.
610        K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
611        // The number-literal kinds already carry PG's `at or near` form.
612        other => alloc::format!(
613            "{}",
614            lexer::LexError {
615                kind: other.clone(),
616                pos: e.pos,
617            }
618        ),
619    };
620    ParseError {
621        message,
622        token_pos: 0,
623    }
624}
625
626/// The offending token exactly as it appears in the input, or `None` at
627/// end of input. PG echoes the source spelling — a lower-case `frm`
628/// reports as `frm`, not as a canonicalised keyword.
629fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
630    let start = *offsets.get(token_pos)?;
631    if start >= input.len() {
632        return None;
633    }
634    let end = offsets
635        .get(token_pos + 1)
636        .copied()
637        .unwrap_or(input.len())
638        .min(input.len());
639    let seg = input.get(start..end)?.trim();
640    if seg.is_empty() {
641        return None;
642    }
643    // A quoted literal / identifier keeps its inner spaces; anything else
644    // ends at the first whitespace (the segment runs to the NEXT token's
645    // start, which may swallow a comment).
646    if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
647        Some(seg)
648    } else {
649        seg.split_whitespace().next()
650    }
651}
652
653/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
654/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
655/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
656/// this re-tokenizes `input` on the cold error path to map the failing token
657/// index to its start byte, then to a character offset. `backslash_escapes`
658/// must match the parse that produced `token_pos` (it barely shifts offsets,
659/// but stay consistent). Returns `None` when the index has no offset or the
660/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
661#[must_use]
662pub fn syntax_error_position(
663    input: &str,
664    backslash_escapes: bool,
665    token_pos: usize,
666) -> Option<usize> {
667    let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
668    let byte_off = *offsets.get(token_pos)?;
669    if byte_off > input.len() || !input.is_char_boundary(byte_off) {
670        return None;
671    }
672    Some(input[..byte_off].chars().count() + 1)
673}
674
675struct Parser {
676    tokens: Vec<Token>,
677    pos: usize,
678    /// v7.39 (round 274) — the session's dialect, carried by the same
679    /// signal that drives string-literal escaping: `SET sql_mode` (only
680    /// MySQL clients and mysqldump preambles emit it) turns it on,
681    /// `SET standard_conforming_strings` (every pg_dump preamble) turns
682    /// it off. Needed here because the two dialects disagree about what
683    /// `REAL` means — see the type mapping below.
684    mysql_dialect: bool,
685    /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
686    /// mutually recursive expr/select parsers. Bounded so a deeply
687    /// nested input returns a parse error instead of overflowing
688    /// the stack (embed hosts die on overflow — it is an abort,
689    /// not a catchable error).
690    nest_depth: usize,
691    /// TABLESAMPLE lowering channel: the table-ref parser pushes a
692    /// `random() < p/100` predicate here; the enclosing SELECT
693    /// drains the list after its WHERE parses and ANDs the
694    /// predicates in. parse_bare_select save/restores around its
695    /// FROM+WHERE so nested selects only drain their own.
696    pending_sample_preds: Vec<Expr>,
697    /// v7.39 (round 691) — collation lowering channel, the same shape as
698    /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
699    /// information, and `ast::OrderBy` is where this parser keeps ordering
700    /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
701    /// variant — puts a new arm on `eval_expr`, which this repo has
702    /// measured to overflow the debug stack. So while an ORDER BY KEY is
703    /// being parsed the postfix loop drops the name here instead of
704    /// refusing it, and the key's parser takes it.
705    ///
706    /// Only inside an ORDER BY key: everywhere else an unperformable
707    /// collation still errors, because accepting one at a COMPARISON and
708    /// ignoring it is the defect F36 exists to close.
709    in_order_by_key: bool,
710    order_key_collation: Option<String>,
711    /// POSITION(sub IN str) — while parsing the needle, the IN
712    /// keyword is the argument separator, not a membership test.
713    /// The postfix loop leaves IN unconsumed when this is set.
714    suppress_in_tail: bool,
715    /// Index of the token the last `advance()` returned — see
716    /// [`Parser::consumed_pos`].
717    last_consumed: usize,
718    /// v7.39 (round 506) — the statement's own text and the byte each token
719    /// starts at, so a MySQL projection item can report the SOURCE TEXT
720    /// MariaDB reports: `SELECT a  +  b` names its column `a  +  b`,
721    /// spacing and all. Only filled for a MySQL session — a PG one names
722    /// columns from the parsed shape and pays nothing for this.
723    src: Option<(String, Vec<usize>)>,
724}
725
726/// Max expr/select parser nesting (parens, subqueries, CASE, …).
727/// Real SQL nests a few dozen levels at the extreme. Each nesting level
728/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
729/// exists to turn a deep statement into a catchable parse ERROR: a stack
730/// overflow is an abort, and in the server it does not fail one query, it
731/// takes the process down and every other connection with it.
732///
733/// v7.39 (round 507) — measured, because the figure here used to be a
734/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
735/// in BOTH debug and release"), and the debug half of that is wrong by
736/// more than an order of magnitude:
737///
738///   * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
739///     this budget and errors. Verified against a live server for nested
740///     derived tables, parens, calls, CASE, IN-subqueries, scalar
741///     subqueries, NOT and unary minus — the server stayed up through all
742///     of them. This is the contract that matters, and it holds.
743///   * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
744///     LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
745///     and executing aborts around 8 inside a test thread. The budget is
746///     simply unreachable there, which is why a deep-nesting test has to
747///     ask for a large stack of its own — see `nesting_budget_errors_at`
748///     in the parser tests.
749/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
750/// one place.
751///
752/// There were two copies of this fact: a curated list, used for BARE
753/// names, and — in `try_peek_meta_qualified` — no list at all, which
754/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
755/// the engine to complain about a view it could not materialise. So
756/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
757/// had rows, `pg_catalog.pg_stat_activity` was an error.
758///
759/// PG puts `pg_catalog` at the implicit front of every search_path, so
760/// the two spellings name the same relation and must resolve the same
761/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
762/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
763/// meta_view_result path under their own names and must not be
764/// rewritten; a name that is neither reaches the ordinary resolver,
765/// which reports that the relation does not exist — PG's answer.
766const SYNTHESISED_PG_CATALOGS: &[&str] = &[
767    "pg_am",
768    "pg_attrdef",
769    "pg_attribute",
770    "pg_cast",
771    "pg_db_role_setting",
772    "pg_conversion",
773    "pg_default_acl",
774    "pg_shadow",
775    "pg_sequences",
776    "pg_range",
777    "pg_partitioned_table",
778    "pg_language",
779    "pg_group",
780    "pg_authid",
781    "pg_class",
782    "pg_collation",
783    "pg_constraint",
784    "pg_database",
785    "pg_depend",
786    "pg_amop",
787    "pg_amproc",
788    "pg_opclass",
789    "pg_opfamily",
790    // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
791    "pg_description",
792    "pg_enum",
793    "pg_extension",
794    // v7.39 (round 541) — pg_dump reads it for every relation of kind
795    // 'f'. SPG has no foreign tables, so it is empty, which is also
796    // what PG reports on a database that has none.
797    "pg_foreign_table",
798    // v7.39 (round 541) — the empty-by-truth family; see
799    // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
800    "pg_event_trigger",
801    "pg_file_settings",
802    "pg_foreign_data_wrapper",
803    "pg_foreign_server",
804    "pg_hba_file_rules",
805    "pg_ident_file_mappings",
806    "pg_init_privs",
807    "pg_parameter_acl",
808    "pg_prepared_xacts",
809    "pg_publication_namespace",
810    "pg_publication_rel",
811    "pg_publication_tables",
812    "pg_replication_origin",
813    "pg_replication_origin_status",
814    "pg_seclabel",
815    "pg_seclabels",
816    "pg_shdepend",
817    "pg_shdescription",
818    "pg_shmem_allocations",
819    "pg_shmem_allocations_numa",
820    "pg_shseclabel",
821    "pg_statistic_ext_data",
822    "pg_stats_ext",
823    "pg_stats_ext_exprs",
824    "pg_subscription_rel",
825    "pg_transform",
826    "pg_user_mapping",
827    "pg_user_mappings",
828    "pg_index",
829    "pg_indexes",
830    "pg_inherits",
831    // v7.39 (round 650) — the text-search catalogs SPG can fill
832    // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
833    // token types to dictionaries and SPG has no token-type model,
834    // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
835    "pg_ts_config",
836    "pg_ts_config_map",
837    "pg_ts_dict",
838    "pg_ts_parser",
839    "pg_ts_template",
840    "pg_matviews",
841    "pg_namespace",
842    // v7.39 (round 621)
843    "pg_operator",
844    "pg_policies",
845    "pg_policy",
846    "pg_proc",
847    "pg_publication",
848    "pg_replication_slots",
849    "pg_roles",
850    // v7.39 (round 143) — the rewrite-rule listing view.
851    // v7.39 (round 312) — and the rule catalogue itself, which
852    // `pg_get_ruledef(oid)` resolves against.
853    "pg_rewrite",
854    "pg_rules",
855    "pg_sequence",
856    "pg_settings",
857    "pg_stat_archiver",
858    "pg_stat_bgwriter",
859    "pg_stat_checkpointer",
860    "pg_stat_database",
861    "pg_stat_io",
862    "pg_stat_progress_analyze",
863    "pg_auth_members",
864    "pg_stat_progress_create_index",
865    "pg_stat_progress_vacuum",
866    "pg_stat_replication",
867    "pg_stat_slru",
868    "pg_stat_subscription_stats",
869    "pg_stat_user_functions",
870    "pg_stat_user_indexes",
871    "pg_stat_user_tables",
872    "pg_stat_wal",
873    "pg_prepared_statements",
874    "pg_largeobject",
875    "pg_largeobject_metadata",
876    "pg_statistic",
877    "pg_statistic_ext",
878    "pg_subscription",
879    "pg_tables",
880    "pg_tablespace",
881    // v7.39 (round 502) — the timezone catalogues. SPG resolved
882    // named zones correctly but could not list them, so a client
883    // populating a timezone picker got "relation does not exist".
884    "pg_timezone_abbrevs",
885    "pg_timezone_names",
886    "pg_trigger",
887    "pg_type",
888    "pg_user",
889    "pg_views",
890];
891
892const MAX_NEST_DEPTH: usize = 64;
893
894/// Stack accounting for the nesting budget, test-only.
895///
896/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
897/// that MOVES: a compiler upgrade grew the parser's debug frames and
898/// silently ate the margin until `nesting_budget_errors_cleanly` went
899/// from erroring cleanly to aborting on a stack overflow. A count
900/// cannot notice that on its own, so the budget is measured here and
901/// held to a ceiling.
902///
903/// The reading has to come from a helper whose OWN frame is the same at
904/// every call: debug slot placement does not follow source order, so a
905/// local's address inside the function under test is not that
906/// function's frame boundary. Two earlier probes were wrong that way —
907/// one read `&self.nest_depth`, which is the `Parser`'s address and
908/// never moves at all.
909#[cfg(test)]
910mod frame_meter {
911    extern crate std;
912    use std::cell::Cell;
913
914    // Per-THREAD, not global. `cargo test` runs tests in parallel and
915    // plenty of them parse nested expressions, so shared statics get
916    // stack addresses from several threads at once and the subtraction
917    // below turns into noise — it read 229,772 bytes per level that way,
918    // while passing when the test was run on its own.
919    std::thread_local! {
920        static AT_LO: Cell<usize> = const { Cell::new(0) };
921        static AT_HI: Cell<usize> = const { Cell::new(0) };
922    }
923
924    pub(super) const SAMPLE_LO: usize = 4;
925    pub(super) const SAMPLE_HI: usize = 24;
926
927    #[inline(never)]
928    pub(super) fn record(depth: usize) {
929        let anchor = 0u8;
930        let at = core::ptr::from_ref(&anchor) as usize;
931        if depth == SAMPLE_LO {
932            AT_LO.with(|c| c.set(at));
933        } else if depth == SAMPLE_HI {
934            AT_HI.with(|c| c.set(at));
935        }
936    }
937
938    /// Bytes of stack one nesting level costs, averaged over the span.
939    pub(super) fn bytes_per_level() -> usize {
940        let lo = AT_LO.with(Cell::get);
941        let hi = AT_HI.with(Cell::get);
942        assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
943        assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
944        (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
945    }
946
947    pub(super) fn reset() {
948        AT_LO.with(|c| c.set(0));
949        AT_HI.with(|c| c.set(0));
950    }
951}
952
953/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
954/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
955#[inline(never)]
956fn build_center_call(e: Expr) -> Expr {
957    Expr::FunctionCall {
958        name: alloc::string::String::from("center"),
959        args: alloc::vec![e],
960    }
961}
962
963/// Max consecutive binary operators at ONE precedence level
964/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
965/// parse time but evaluates and drops recursively — depth beyond
966/// this overflows 2 MiB worker stacks (debug eval frames run
967/// multiple KiB). `IN (…)` lists are flat and unaffected.
968const MAX_BINARY_CHAIN: usize = 256;
969
970/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
971/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
972/// it keeps its dedicated path (`parse_table_level_fk`).
973enum NamedTableConstraintKind {
974    Check,
975    Unique,
976    PrimaryKey,
977    Exclude,
978}
979
980impl Parser {
981    fn new(tokens: Vec<Token>) -> Self {
982        Self::new_with_dialect(tokens, false)
983    }
984
985    fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
986        Self {
987            tokens,
988            mysql_dialect,
989            in_order_by_key: false,
990            order_key_collation: None,
991            pos: 0,
992            nest_depth: 0,
993            pending_sample_preds: Vec::new(),
994            suppress_in_tail: false,
995            last_consumed: 0,
996            src: None,
997        }
998    }
999
1000    /// Hand the parser the text it is parsing, for [`Parser::source_span`].
1001    fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
1002        if self.mysql_dialect {
1003            self.src = Some((input.to_string(), offsets.to_vec()));
1004        }
1005        self
1006    }
1007
1008    /// The source text spanning tokens `start ..= end`, trimmed.
1009    ///
1010    /// The offsets are token STARTS, so the span runs to the start of the
1011    /// token after `end` and gives back the whitespace between them —
1012    /// trimming is what makes `a + b FROM t` end at `b`.
1013    fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1014        let (text, offsets) = self.src.as_ref()?;
1015        let from = *offsets.get(start)?;
1016        let to = *offsets.get(end + 1)?;
1017        text.get(from..to).map(str::trim_end)
1018    }
1019
1020    /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1021    /// nesting depth, erroring out cleanly past the budget.
1022    fn enter_nested(&mut self) -> Result<(), ParseError> {
1023        self.nest_depth += 1;
1024        #[cfg(test)]
1025        frame_meter::record(self.nest_depth);
1026        if self.nest_depth > MAX_NEST_DEPTH {
1027            self.nest_depth -= 1;
1028            return Err(self.err(alloc::format!(
1029                "statement nests deeper than {MAX_NEST_DEPTH} levels"
1030            )));
1031        }
1032        Ok(())
1033    }
1034
1035    fn peek(&self) -> &Token {
1036        // tokens always ends with Eof; pos is clamped in advance().
1037        &self.tokens[self.pos]
1038    }
1039
1040    fn advance(&mut self) -> Token {
1041        let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1042        self.last_consumed = self.pos;
1043        if self.pos + 1 < self.tokens.len() {
1044            self.pos += 1;
1045        }
1046        t
1047    }
1048
1049    /// v7.39 (round 340, V56) — the index of the token `advance()` just
1050    /// returned. It was computed as `pos - 1`, which is wrong at both
1051    /// ends: `advance()` parks on the final Eof rather than running off
1052    /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1053    /// input`), and after backtracking `pos` is no longer one past the
1054    /// token that failed. Recorded by `advance()` itself instead.
1055    const fn consumed_pos(&self) -> usize {
1056        self.last_consumed
1057    }
1058
1059    fn err(&self, message: String) -> ParseError {
1060        ParseError {
1061            message,
1062            token_pos: self.pos,
1063        }
1064    }
1065
1066    fn expect_eof(&self) -> Result<(), ParseError> {
1067        if matches!(self.peek(), Token::Eof) {
1068            Ok(())
1069        } else {
1070            Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1071        }
1072    }
1073
1074    /// v7.14.0 — swallow every token up to (but not including) the
1075    /// next semicolon / EOF. Used by the dump-noise dispatcher
1076    /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1077    /// etc. without modeling each grammar.
1078    /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1079    /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1080    /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1081    /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1082    /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1083    fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1084        let start = self.pos;
1085        self.advance(); // COMMENT
1086        if !matches!(self.peek(), Token::On) {
1087            self.pos = start;
1088            self.consume_until_statement_boundary();
1089            return Ok(Statement::Empty);
1090        }
1091        self.advance(); // ON
1092        let kind = match self.peek() {
1093            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1094            Token::Table => "table".into(),
1095            _ => {
1096                self.consume_until_statement_boundary();
1097                return Ok(Statement::Empty);
1098            }
1099        };
1100        if !matches!(
1101            kind.as_str(),
1102            "table"
1103                | "column"
1104                | "index"
1105                | "view"
1106                | "sequence"
1107                | "schema"
1108                | "type"
1109                | "database"
1110                | "function"
1111        ) {
1112            self.consume_until_statement_boundary();
1113            return Ok(Statement::Empty);
1114        }
1115        self.advance(); // the kind keyword
1116        // The object name. ⚠️ `expect_ident_like` strips a leading
1117        // `<schema>.` qualifier and returns only the trailing ident (SPG is
1118        // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1119        // `c`. Read the dotted parts from raw tokens instead, then let a
1120        // 3-part `schema.t.c` drop its leading schema like everywhere else.
1121        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1122        loop {
1123            match self.advance() {
1124                Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1125                other if unreserved_keyword_text(&other).is_some() => {
1126                    parts.push(unreserved_keyword_text(&other).unwrap());
1127                }
1128                other => {
1129                    return Err(ParseError {
1130                        message: alloc::format!("expected identifier, got {other:?}"),
1131                        token_pos: self.consumed_pos(),
1132                    });
1133                }
1134            }
1135            if matches!(self.peek(), Token::Dot) {
1136                self.advance();
1137            } else {
1138                break;
1139            }
1140        }
1141        // COLUMN wants `table.column`; every other kind wants a bare name.
1142        let want = if kind == "column" { 2 } else { 1 };
1143        while parts.len() > want {
1144            parts.remove(0);
1145        }
1146        let name = parts.join(".");
1147        // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1148        // pg_dump writes the SIGNATURE, and the paren list was a syntax
1149        // error here — a dump carrying one function comment failed to
1150        // restore. The list is consumed (the comment store keys by name;
1151        // overload-precise comments are the function-predicate follow-up).
1152        if matches!(self.peek(), Token::LParen)
1153            && matches!(
1154                kind.as_str(),
1155                "function" | "procedure" | "aggregate" | "routine"
1156            )
1157        {
1158            let mut depth = 0usize;
1159            loop {
1160                match self.advance() {
1161                    Token::LParen => depth += 1,
1162                    Token::RParen => {
1163                        depth -= 1;
1164                        if depth == 0 {
1165                            break;
1166                        }
1167                    }
1168                    Token::Eof => {
1169                        return Err(self.err(alloc::string::String::from(
1170                            "unterminated argument list in COMMENT ON",
1171                        )));
1172                    }
1173                    _ => {}
1174                }
1175            }
1176        }
1177        // `IS`
1178        if !matches!(self.peek(), Token::Is) {
1179            self.expect_keyword_ident("is")?;
1180        } else {
1181            self.advance();
1182        }
1183        let comment = match self.peek() {
1184            Token::Null => {
1185                self.advance();
1186                None
1187            }
1188            _ => Some(self.expect_string_literal()?),
1189        };
1190        Ok(Statement::CommentOn {
1191            kind,
1192            name,
1193            comment,
1194        })
1195    }
1196
1197    /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1198    /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1199    /// [CASCADE|RESTRICT]`.
1200    ///
1201    /// TABLE privileges are the real ones (stored, enforced, introspectable).
1202    /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1203    /// and the no-ON `GRANT role TO role` membership form — parses into
1204    /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1205    /// on them still restores.
1206    fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1207        self.advance(); // GRANT / REVOKE
1208        // REVOKE's optional `GRANT OPTION FOR` prefix.
1209        let mut grant_option = false;
1210        if !grant
1211            && self.peek_keyword_ident("grant")
1212            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1213        {
1214            self.advance(); // GRANT
1215            self.advance(); // OPTION
1216            self.expect_keyword_ident("for")?;
1217            grant_option = true;
1218        }
1219        // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1220        // words each with an optional COLUMN list.
1221        let mut privileges: Vec<GrantPriv> = Vec::new();
1222        if matches!(self.peek(), Token::All) {
1223            self.advance();
1224            if self.peek_keyword_ident("privileges") {
1225                self.advance();
1226            }
1227            // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1228            // column only.
1229            if matches!(self.peek(), Token::LParen) {
1230                let columns = self.parse_grant_column_list()?;
1231                privileges.push(GrantPriv {
1232                    word: "ALL".into(),
1233                    columns,
1234                });
1235            }
1236        } else {
1237            loop {
1238                // SELECT and INSERT lex as reserved tokens, so they never
1239                // reach `expect_ident_like` as plain idents; the rest
1240                // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1241                // MAINTAIN) are ordinary identifiers.
1242                let w = match self.peek() {
1243                    Token::Select => {
1244                        self.advance();
1245                        "SELECT".to_string()
1246                    }
1247                    Token::Insert => {
1248                        self.advance();
1249                        "INSERT".to_string()
1250                    }
1251                    // v7.39 (read01 round 60) — CREATE is a privilege word on a
1252                    // schema / database, and it lexes as a reserved token.
1253                    Token::Create => {
1254                        self.advance();
1255                        "CREATE".to_string()
1256                    }
1257                    // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1258                    // alice`) these "privilege words" are ROLE NAMES, and a
1259                    // role name is case-sensitive. `priv_from_word` folds case
1260                    // itself when they really are privileges.
1261                    _ => self.expect_ident_like()?,
1262                };
1263                // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1264                // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1265                let columns = if matches!(self.peek(), Token::LParen) {
1266                    self.parse_grant_column_list()?
1267                } else {
1268                    Vec::new()
1269                };
1270                privileges.push(GrantPriv { word: w, columns });
1271                if matches!(self.peek(), Token::Comma) {
1272                    self.advance();
1273                } else {
1274                    break;
1275                }
1276            }
1277        }
1278        // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1279        // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1280        if !matches!(self.peek(), Token::On) {
1281            let roles: Vec<String> = core::mem::take(&mut privileges)
1282                .into_iter()
1283                .map(|p| p.word)
1284                .collect();
1285            let grantees = self.parse_grantee_list(grant)?;
1286            // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1287            // no admin-option layer: a member cannot re-grant).
1288            self.consume_until_statement_boundary();
1289            return Ok(finish_grant(
1290                grant,
1291                GrantStatement {
1292                    privileges: Vec::new(),
1293                    object: GrantObject::Roles(roles),
1294                    grantees,
1295                    grant_option,
1296                },
1297            ));
1298        }
1299        self.advance(); // ON
1300        // An optional object-class keyword. `TABLE` (or no keyword at all) is
1301        // the enforced case; anything else parses and no-ops.
1302        let mut class = "TABLE";
1303        match self.peek() {
1304            Token::Table => {
1305                self.advance();
1306            }
1307            Token::All => {
1308                // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1309                // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1310                // IN SCHEMA` stay no-ops and keep their own object class.
1311                self.advance(); // ALL
1312                let kind = match self.peek() {
1313                    Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1314                    // TABLES has its own token (SHOW TABLES owns it).
1315                    Token::Tables | Token::Table => "tables".to_string(),
1316                    _ => String::new(),
1317                };
1318                if !kind.is_empty() {
1319                    self.advance();
1320                }
1321                // `IN SCHEMA <name>`
1322                if matches!(self.peek(), Token::In) {
1323                    self.advance();
1324                    if self.peek_keyword_ident("schema") {
1325                        self.advance();
1326                        let _schema = self.expect_ident_like()?;
1327                    }
1328                }
1329                if kind != "tables" {
1330                    self.consume_until_statement_boundary();
1331                    return Ok(finish_grant(
1332                        grant,
1333                        GrantStatement {
1334                            privileges,
1335                            object: GrantObject::Other("ALL … IN SCHEMA".into()),
1336                            grantees: Vec::new(),
1337                            grant_option,
1338                        },
1339                    ));
1340                }
1341                let grantees = self.parse_grantee_list(grant)?;
1342                self.consume_until_statement_boundary();
1343                return Ok(finish_grant(
1344                    grant,
1345                    GrantStatement {
1346                        privileges,
1347                        object: GrantObject::AllTablesInSchema,
1348                        grantees,
1349                        grant_option,
1350                    },
1351                ));
1352            }
1353            Token::Ident(w) | Token::QuotedIdent(w) => {
1354                let lc = w.to_ascii_lowercase();
1355                // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1356                // real objects with real ACLs now.
1357                if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1358                    self.advance();
1359                    let mut names: Vec<String> = Vec::new();
1360                    loop {
1361                        let mut parts: Vec<String> = Vec::new();
1362                        loop {
1363                            parts.push(self.expect_ident_like()?);
1364                            if matches!(self.peek(), Token::Dot) {
1365                                self.advance();
1366                            } else {
1367                                break;
1368                            }
1369                        }
1370                        names.push(parts.pop().expect("at least one part"));
1371                        if matches!(self.peek(), Token::Comma) {
1372                            self.advance();
1373                        } else {
1374                            break;
1375                        }
1376                    }
1377                    let grantees = self.parse_grantee_list(grant)?;
1378                    let mut grant_option = grant_option;
1379                    if grant && self.peek_keyword_ident("with") {
1380                        self.advance();
1381                        self.expect_keyword_ident("grant")?;
1382                        self.expect_keyword_ident("option")?;
1383                        grant_option = true;
1384                    }
1385                    self.consume_until_statement_boundary();
1386                    let object = match lc.as_str() {
1387                        "sequence" => GrantObject::Sequences(names),
1388                        "schema" => GrantObject::Schemas(names),
1389                        _ => GrantObject::Databases(names),
1390                    };
1391                    return Ok(finish_grant(
1392                        grant,
1393                        GrantStatement {
1394                            privileges,
1395                            object,
1396                            grantees,
1397                            grant_option,
1398                        },
1399                    ));
1400                }
1401                // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1402                // keys functions by NAME, so the argument list parses and is
1403                // dropped (an overload set shares one ACL — recorded residual).
1404                if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1405                    self.advance();
1406                    let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1407                    loop {
1408                        let mut parts: Vec<String> = Vec::new();
1409                        loop {
1410                            parts.push(self.expect_ident_like()?);
1411                            if matches!(self.peek(), Token::Dot) {
1412                                self.advance();
1413                            } else {
1414                                break;
1415                            }
1416                        }
1417                        let fname = parts.pop().expect("at least one part");
1418                        // v7.39 (read01 round 62) — the signature picks the
1419                        // overload, so it is captured.
1420                        let sig = if matches!(self.peek(), Token::LParen) {
1421                            Some(self.parse_function_signature_types()?)
1422                        } else {
1423                            None
1424                        };
1425                        names.push((fname, sig));
1426                        if matches!(self.peek(), Token::Comma) {
1427                            self.advance();
1428                        } else {
1429                            break;
1430                        }
1431                    }
1432                    let grantees = self.parse_grantee_list(grant)?;
1433                    self.consume_until_statement_boundary();
1434                    return Ok(finish_grant(
1435                        grant,
1436                        GrantStatement {
1437                            privileges,
1438                            object: GrantObject::Functions(names),
1439                            grantees,
1440                            grant_option,
1441                        },
1442                    ));
1443                }
1444                if matches!(
1445                    lc.as_str(),
1446                    "type"
1447                        | "domain"
1448                        | "language"
1449                        | "tablespace"
1450                        | "large"
1451                        | "foreign"
1452                        | "parameter"
1453                ) {
1454                    self.consume_until_statement_boundary();
1455                    return Ok(finish_grant(
1456                        grant,
1457                        GrantStatement {
1458                            privileges,
1459                            object: GrantObject::Other(lc.to_ascii_uppercase()),
1460                            grantees: Vec::new(),
1461                            grant_option,
1462                        },
1463                    ));
1464                }
1465                class = "TABLE";
1466            }
1467            _ => {}
1468        }
1469        let _ = class;
1470        // The table list. Schema-qualified names drop their qualifier (SPG is
1471        // single-schema) — but read the dotted parts from raw tokens, since
1472        // `expect_ident_like` would silently swallow the leading part.
1473        let mut tables: Vec<String> = Vec::new();
1474        loop {
1475            let mut parts: Vec<String> = Vec::new();
1476            loop {
1477                parts.push(self.expect_ident_like()?);
1478                if matches!(self.peek(), Token::Dot) {
1479                    self.advance();
1480                } else {
1481                    break;
1482                }
1483            }
1484            tables.push(parts.pop().expect("at least one part"));
1485            if matches!(self.peek(), Token::Comma) {
1486                self.advance();
1487            } else {
1488                break;
1489            }
1490        }
1491        let grantees = self.parse_grantee_list(grant)?;
1492        if grant && self.peek_keyword_ident("with") {
1493            self.advance();
1494            self.expect_keyword_ident("grant")?;
1495            self.expect_keyword_ident("option")?;
1496            grant_option = true;
1497        }
1498        // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1499        // to cascade to (no re-granting), so both are accepted and ignored.
1500        if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1501            self.advance();
1502        }
1503        Ok(finish_grant(
1504            grant,
1505            GrantStatement {
1506                privileges,
1507                object: GrantObject::Tables(tables),
1508                grantees,
1509                grant_option,
1510            },
1511        ))
1512    }
1513
1514    /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1515    /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1516    /// words; the caller normalises them into a signature key.
1517    fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1518        self.advance(); // (
1519        let mut types: Vec<String> = Vec::new();
1520        if matches!(self.peek(), Token::RParen) {
1521            self.advance();
1522            return Ok(types);
1523        }
1524        loop {
1525            // Collect the words of one argument up to a comma / close paren.
1526            let mut words: Vec<String> = Vec::new();
1527            loop {
1528                match self.peek() {
1529                    Token::Comma | Token::RParen | Token::Eof => break,
1530                    _ => {}
1531                }
1532                let tok = self.advance();
1533                match tok {
1534                    Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1535                    other => {
1536                        if let Some(w) = unreserved_keyword_text(&other) {
1537                            words.push(w);
1538                        }
1539                    }
1540                }
1541            }
1542            // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1543            // themselves several words (`double precision`, `character
1544            // varying`, `timestamp with time zone`), so "two words means the
1545            // first is a parameter name" reads the type off `f(double
1546            // precision)` as `precision`. v7.39 (round 282): recognise the
1547            // multi-word spellings first — a leading word that STARTS one of
1548            // them is part of the type, not a name.
1549            let joined = words.join(" ");
1550            let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1551                joined
1552            } else if words.len() >= 2 {
1553                words[1..].join(" ")
1554            } else {
1555                words.first().cloned().unwrap_or_default()
1556            };
1557            types.push(ty);
1558            if matches!(self.peek(), Token::Comma) {
1559                self.advance();
1560            } else {
1561                break;
1562            }
1563        }
1564        if matches!(self.peek(), Token::RParen) {
1565            self.advance();
1566        }
1567        Ok(types)
1568    }
1569
1570    /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1571    fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1572        self.advance(); // (
1573        let mut cols = Vec::new();
1574        loop {
1575            cols.push(self.expect_ident_like()?);
1576            if matches!(self.peek(), Token::Comma) {
1577                self.advance();
1578            } else {
1579                break;
1580            }
1581        }
1582        if !matches!(self.peek(), Token::RParen) {
1583            return Err(self.err(alloc::format!(
1584                "expected ')' to close the column list, got {:?}",
1585                self.peek()
1586            )));
1587        }
1588        self.advance(); // )
1589        Ok(cols)
1590    }
1591
1592    /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1593    /// PUBLIC.
1594    fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1595        if grant {
1596            if matches!(self.peek(), Token::To) {
1597                self.advance();
1598            } else {
1599                self.expect_keyword_ident("to")?;
1600            }
1601        } else if matches!(self.peek(), Token::From) {
1602            self.advance();
1603        } else {
1604            self.expect_keyword_ident("from")?;
1605        }
1606        let mut grantees: Vec<String> = Vec::new();
1607        loop {
1608            // `GROUP name` is the legacy spelling of a plain role name.
1609            if self.peek_keyword_ident("group") {
1610                self.advance();
1611            }
1612            if self.peek_keyword_ident("public") {
1613                self.advance();
1614                grantees.push(String::new()); // PUBLIC
1615            } else {
1616                grantees.push(self.expect_ident_like()?);
1617            }
1618            if matches!(self.peek(), Token::Comma) {
1619                self.advance();
1620            } else {
1621                break;
1622            }
1623        }
1624        Ok(grantees)
1625    }
1626
1627    /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1628    /// The body keeps its `$N` placeholders; substitution happens at
1629    /// EXECUTE. The declared types are recorded for
1630    /// `pg_prepared_statements.parameter_types` but are not enforced —
1631    /// PG infers when the list is omitted, and SPG resolves the values
1632    /// at substitution time either way.
1633    fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1634        let start = self.pos;
1635        self.advance(); // PREPARE
1636        // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1637        // different statement that happens to share the keyword. PG
1638        // ships with `max_prepared_transactions = 0` and reports it
1639        // this way; SPG has no prepared-transaction registry, so the
1640        // same wording is the accurate answer rather than a dodge.
1641        // Round 277 turned this from a silent no-op into a confusing
1642        // "expected AS in PREPARE" parse error.
1643        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1644            self.advance();
1645            let gid = match self.advance() {
1646                Token::String(g) => g,
1647                other => {
1648                    return Err(self.err(alloc::format!(
1649                        "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1650                    )));
1651                }
1652            };
1653            return Ok(Statement::PrepareTransaction(gid));
1654        }
1655        let name = self.expect_ident_like()?;
1656        let mut param_types = Vec::new();
1657        if matches!(self.peek(), Token::LParen) {
1658            self.advance();
1659            loop {
1660                let mut ty = self.expect_ident_like()?;
1661                // A parameterised type name (`numeric(10,2)`,
1662                // `varchar(20)`) keeps its argument list in the text.
1663                if matches!(self.peek(), Token::LParen) {
1664                    let mut depth = 0usize;
1665                    let mut buf = String::from("(");
1666                    loop {
1667                        match self.advance() {
1668                            Token::LParen => {
1669                                depth += 1;
1670                                if depth > 1 {
1671                                    buf.push('(');
1672                                }
1673                            }
1674                            Token::RParen => {
1675                                depth -= 1;
1676                                buf.push(')');
1677                                if depth == 0 {
1678                                    break;
1679                                }
1680                            }
1681                            Token::Comma => buf.push(','),
1682                            Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1683                            Token::Eof => break,
1684                            _ => {}
1685                        }
1686                    }
1687                    ty.push_str(&buf);
1688                }
1689                // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1690                // position, same family as the parameter list above.
1691                let array_suffix = self.consume_array_suffix();
1692                ty.push_str(&array_suffix);
1693                param_types.push(ty);
1694                match self.peek() {
1695                    Token::Comma => {
1696                        self.advance();
1697                    }
1698                    Token::RParen => {
1699                        self.advance();
1700                        break;
1701                    }
1702                    other => {
1703                        return Err(self.err(alloc::format!(
1704                            "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1705                        )));
1706                    }
1707                }
1708            }
1709        }
1710        if !matches!(self.peek(), Token::As) {
1711            return Err(self.err(alloc::format!(
1712                "expected AS in PREPARE, got {:?}",
1713                self.peek()
1714            )));
1715        }
1716        self.advance();
1717        let body = self.parse_one_statement()?;
1718        // The Parser holds tokens, not the source text, so the
1719        // statement PG reports in `pg_prepared_statements.statement`
1720        // is rebuilt from the AST rather than sliced from the input.
1721        let _ = start;
1722        let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1723        if !param_types.is_empty() {
1724            source.push_str(" (");
1725            source.push_str(&param_types.join(", "));
1726            source.push(')');
1727        }
1728        source.push_str(" AS ");
1729        source.push_str(&alloc::format!("{body}"));
1730        Ok(Statement::Prepare {
1731            name,
1732            param_types,
1733            body: alloc::boxed::Box::new(body),
1734            source,
1735        })
1736    }
1737
1738    /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1739    fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1740        self.advance(); // EXECUTE
1741        let name = self.expect_ident_like()?;
1742        let mut args = Vec::new();
1743        if matches!(self.peek(), Token::LParen) {
1744            self.advance();
1745            if matches!(self.peek(), Token::RParen) {
1746                self.advance();
1747            } else {
1748                loop {
1749                    args.push(self.parse_expr(0)?);
1750                    match self.advance() {
1751                        Token::Comma => {}
1752                        Token::RParen => break,
1753                        other => {
1754                            return Err(self.err(alloc::format!(
1755                                "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1756                            )));
1757                        }
1758                    }
1759                }
1760            }
1761        }
1762        Ok(Statement::Execute { name, args })
1763    }
1764
1765    /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1766    /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1767    /// procedure catalog yet, so this reports PG's not-found error
1768    /// (with its HINT) rather than pretending the call ran.
1769    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1770    /// Bare `DISCARD` is a syntax error in PG; so it is here.
1771    fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1772        self.advance(); // DISCARD
1773        let target = match self.advance() {
1774            Token::All => DiscardTarget::All,
1775            Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1776                "all" => DiscardTarget::All,
1777                "plans" => DiscardTarget::Plans,
1778                "sequences" => DiscardTarget::Sequences,
1779                "temp" | "temporary" => DiscardTarget::Temp,
1780                other => {
1781                    return Err(self.err(format!(
1782                        "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1783                    )));
1784                }
1785            },
1786            other => {
1787                return Err(self.err(format!(
1788                    "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1789                )));
1790            }
1791        };
1792        Ok(Statement::Discard(target))
1793    }
1794
1795    /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1796    /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1797    /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1798    /// aggressively the server interrupts, which SPG does not distinguish.
1799    /// Bare `KILL <id>` means CONNECTION.
1800    fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1801        self.advance(); // KILL
1802        let mut query_only = false;
1803        loop {
1804            // CONNECTION is a reserved keyword token (it also opens
1805            // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1806            // `Token::Connection` rather than a bare ident.
1807            if matches!(self.peek(), Token::Connection) {
1808                self.advance();
1809                break;
1810            }
1811            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1812                break;
1813            };
1814            match w.to_ascii_lowercase().as_str() {
1815                "hard" | "soft" => {
1816                    self.advance();
1817                }
1818                "query" => {
1819                    self.advance();
1820                    query_only = true;
1821                    break;
1822                }
1823                _ => break,
1824            }
1825        }
1826        let id = self.parse_expr(0)?;
1827        Ok(Statement::Kill {
1828            query_only,
1829            id: Box::new(id),
1830        })
1831    }
1832
1833    fn parse_call(&mut self) -> Result<Statement, ParseError> {
1834        self.advance(); // CALL
1835        let name = self.expect_ident_like()?;
1836        self.consume_until_statement_boundary();
1837        Ok(Statement::Call(name))
1838    }
1839
1840    fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1841        self.advance(); // DEALLOCATE
1842        // PG accepts an optional noise `PREPARE` keyword here.
1843        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1844            self.advance();
1845        }
1846        if matches!(self.peek(), Token::All) {
1847            self.advance();
1848            return Ok(Statement::Deallocate(None));
1849        }
1850        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1851            self.advance();
1852            return Ok(Statement::Deallocate(None));
1853        }
1854        let name = self.expect_ident_like()?;
1855        Ok(Statement::Deallocate(Some(name)))
1856    }
1857
1858    fn consume_until_statement_boundary(&mut self) {
1859        loop {
1860            match self.peek() {
1861                Token::Semicolon | Token::Eof => return,
1862                _ => self.advance(),
1863            };
1864        }
1865    }
1866
1867    /// v7.22 (round-13 T2) — consume to the statement boundary like
1868    /// `consume_until_statement_boundary`, but pick out the sequence
1869    /// name on the way: either `SEQUENCE NAME <ident>` (identity
1870    /// columns) or the first string literal (`nextval('<seq>')`).
1871    /// Schema qualifiers and `::regclass` casts are stripped.
1872    fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1873        let mut seq: Option<String> = None;
1874        let mut after_sequence_kw = false;
1875        let mut after_name_kw = false;
1876        loop {
1877            match self.peek().clone() {
1878                Token::Semicolon | Token::Eof => break,
1879                Token::Ident(s) | Token::QuotedIdent(s) => {
1880                    if after_name_kw && seq.is_none() {
1881                        self.advance();
1882                        let mut name = s;
1883                        // `SEQUENCE NAME public.groups_id_seq` — keep
1884                        // the bare name, drop qualifiers.
1885                        while matches!(self.peek(), Token::Dot) {
1886                            self.advance();
1887                            if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1888                                name = n;
1889                            }
1890                        }
1891                        seq = Some(name);
1892                        after_name_kw = false;
1893                        continue;
1894                    }
1895                    if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1896                        after_name_kw = true;
1897                        after_sequence_kw = false;
1898                    } else {
1899                        after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1900                    }
1901                    self.advance();
1902                }
1903                Token::String(s) => {
1904                    if seq.is_none() {
1905                        // `nextval('public.groups_id_seq'::regclass)`
1906                        let bare = s
1907                            .rsplit_once('.')
1908                            .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1909                        seq = Some(bare);
1910                    }
1911                    self.advance();
1912                }
1913                _ => {
1914                    after_sequence_kw = false;
1915                    after_name_kw = false;
1916                    self.advance();
1917                }
1918            }
1919        }
1920        seq
1921    }
1922
1923    /// v7.39 (round 621) — is the next token the keyword `BY`?
1924    ///
1925    /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
1926    /// column, table and alias name — and SPG lexed it into a dedicated
1927    /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
1928    /// two-letter keywords the lexer knew, this was the only one PG leaves
1929    /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
1930    ///
1931    /// The token is gone; the three clauses that own the word — GROUP BY,
1932    /// ORDER BY, PARTITION BY — and the handful of other places that expect it
1933    /// ask this instead. Adding it to the unreserved-identifier table was not
1934    /// enough on its own: identifier positions that match the token shape
1935    /// directly (an index's column list, a table alias) never consult that
1936    /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
1937    /// Not lexing it as a keyword closes the whole class rather than the two
1938    /// positions that happened to be noticed.
1939    fn peek_is_by(&self) -> bool {
1940        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
1941    }
1942
1943    /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
1944    /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
1945    /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
1946    fn consume_drop_behaviour(&mut self) {
1947        if matches!(
1948            self.peek(),
1949            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
1950        ) {
1951            self.advance();
1952        }
1953    }
1954
1955    fn expect_ident_like(&mut self) -> Result<String, ParseError> {
1956        let first = match self.advance() {
1957            Token::Ident(s) | Token::QuotedIdent(s) => s,
1958            // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
1959            // per PG's `pg_get_keywords()` classification. SPG tokenizes
1960            // these as named variants for parsing leverage in the
1961            // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
1962            // `BEGIN`, etc.), but they MUST still be usable as table /
1963            // column / alias names in DDL+DML. Sentori migrations like
1964            // 0001_init.sql ship `release TEXT NOT NULL` in the events
1965            // table — the `events.release` column carries the release
1966            // identifier string. Pre-T4 this triggered "expected
1967            // identifier, got Release" and blocked every drop-in user
1968            // whose schema had a column / alias with one of these names.
1969            other if unreserved_keyword_text(&other).is_some() => {
1970                unreserved_keyword_text(&other).unwrap()
1971            }
1972            other => {
1973                return Err(ParseError {
1974                    message: format!("expected identifier, got {other:?}"),
1975                    token_pos: self.consumed_pos(),
1976                });
1977            }
1978        };
1979        // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
1980        // qualify every name with `public.` (and pg_catalog.* for
1981        // functions); SPG is single-schema so we discard the
1982        // prefix and return only the trailing ident. Same shape
1983        // also handles MySQL `db.tbl` cross-database refs (SPG
1984        // ignores the db part).
1985        if matches!(self.peek(), Token::Dot) {
1986            self.advance();
1987            match self.advance() {
1988                Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
1989                other if unreserved_keyword_text(&other).is_some() => {
1990                    return Ok(unreserved_keyword_text(&other).unwrap());
1991                }
1992                other => {
1993                    return Err(ParseError {
1994                        message: format!("expected identifier after '{first}.', got {other:?}"),
1995                        token_pos: self.consumed_pos(),
1996                    });
1997                }
1998            }
1999        }
2000        Ok(first)
2001    }
2002
2003    #[allow(clippy::too_many_lines)]
2004    fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2005        // v7.14.0 — empty / comment-only / semicolon-only input
2006        // (after the lexer strips line + block + MySQL
2007        // conditional comments) lands as Statement::Empty.
2008        // pg_dump and mysqldump emit several wrappers that
2009        // collapse to nothing after stripping (`/*!40101 SET …
2010        // */;`, blank lines between statements); the engine
2011        // returns CommandOk no-op so the dump loads cleanly.
2012        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2013            return Ok(Statement::Empty);
2014        }
2015        // v7.14.0 — pg_dump / mysqldump "noise" statements:
2016        // catalog / metadata DDL that has no behavioural effect
2017        // on SPG's single-schema, single-database, single-user
2018        // model. Consume the whole statement up to the next
2019        // semicolon / EOF and return Empty. This is broader than
2020        // the per-keyword DROP / SET / COMMENT arms but lets the
2021        // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2022        // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2023        // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2024        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2025            let lc = s.to_ascii_lowercase();
2026            // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2027            if lc == "comment" {
2028                return self.parse_comment_on();
2029            }
2030            // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2031            if lc == "grant" || lc == "revoke" {
2032                return self.parse_grant_or_revoke(lc == "grant");
2033            }
2034            // v7.39 (round 277) — the SQL-level prepared-statement
2035            // surface is REAL now. It used to be accepted and dropped
2036            // on the theory that "real execution still happens via the
2037            // extended-query flow" — true only for a driver that uses
2038            // that flow; a plain SQL PREPARE / EXECUTE returned no
2039            // rows at all.
2040            if lc == "prepare" {
2041                return self.parse_prepare();
2042            }
2043            if lc == "execute" {
2044                return self.parse_execute();
2045            }
2046            if lc == "deallocate" {
2047                return self.parse_deallocate();
2048            }
2049            // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2050            // accepted and dropped, so an application's stored-procedure
2051            // invocation reported success and did nothing. SPG has no
2052            // procedure catalog, so every CALL names a procedure that
2053            // does not exist — which is exactly what PG says.
2054            if lc == "call" {
2055                return self.parse_call();
2056            }
2057            // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2058            // names one connection and acts on it.
2059            if lc == "kill" {
2060                return self.parse_kill();
2061            }
2062            if lc == "discard" {
2063                return self.parse_discard();
2064            }
2065            // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2066            // Still performs nothing; the roles are carried out so a name
2067            // that does not exist is refused, as PG18 refuses it.
2068            if lc == "reassign" {
2069                self.advance();
2070                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2071                    self.advance();
2072                }
2073                if self.peek_is_by() {
2074                    self.advance();
2075                }
2076                // Only the roles BEFORE the TO are the ones that must
2077                // exist — `TO` names the new owner, which PG checks as
2078                // well, so both lists are collected.
2079                let mut names = self.take_comma_separated_names();
2080                if matches!(self.peek(), Token::To) {
2081                    self.advance();
2082                    names.extend(self.take_comma_separated_names());
2083                }
2084                self.consume_until_statement_boundary();
2085                return Ok(Statement::ValidateOnly {
2086                    kind: crate::ast::ValidateOnlyKind::RoleName,
2087                    names,
2088                });
2089            }
2090            // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2091            // unconditionally with `no security label providers have been
2092            // loaded`, whatever object it names, because none is loaded.
2093            // SPG has none either; accepting it told the caller a label had
2094            // been applied when nothing anywhere records one.
2095            if lc == "security" {
2096                self.consume_until_statement_boundary();
2097                return Ok(Statement::ValidateOnly {
2098                    kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2099                    names: Vec::new(),
2100                });
2101            }
2102            if is_dump_noise_statement(&lc) {
2103                self.consume_until_statement_boundary();
2104                return Ok(Statement::Empty);
2105            }
2106        }
2107        match self.peek() {
2108            Token::Select => self.parse_select_stmt(),
2109            // v7.37.17 (17.6 siblings) — a statement opening with a
2110            // parenthesized query group: `(SELECT … UNION …)
2111            // INTERSECT …`. parse_bare_select's group arm consumes
2112            // the parens; the select parser handles the outer chain
2113            // and tail.
2114            Token::LParen
2115                if matches!(
2116                    self.tokens.get(self.pos + 1),
2117                    Some(Token::Select | Token::LParen | Token::Values)
2118                ) =>
2119            {
2120                self.parse_select_stmt()
2121            }
2122            // v7.37.17 (17.6 siblings) — top-level bare VALUES
2123            // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2124            // Lowers to the same UNION ALL chain the FROM-position
2125            // form uses, then reuses the shared SELECT tail.
2126            Token::Values => {
2127                self.advance(); // VALUES
2128                let mut head = self.parse_values_rows_body()?;
2129                self.parse_select_tail_into(&mut head)?;
2130                Ok(Statement::Select(head))
2131            }
2132            // SQL-standard `TABLE name` shorthand for
2133            // `SELECT * FROM name` — pg_dump never emits it, but
2134            // psql users and PG docs use it constantly. Set-op
2135            // chains and the ORDER BY/LIMIT tail compose like any
2136            // SELECT head.
2137            Token::Table
2138                if matches!(
2139                    self.tokens.get(self.pos + 1),
2140                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2141                ) =>
2142            {
2143                let mut head = self.parse_table_shorthand()?;
2144                self.parse_setop_chain_into(&mut head)?;
2145                self.parse_select_tail_into(&mut head)?;
2146                Ok(Statement::Select(head))
2147            }
2148            // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2149            // body is a dollar-quoted plpgsql block (lexer already
2150            // collapsed `$$…$$` into a single Token::String).
2151            // v7.16.2 — mailrs round-10 A.2: parse the body as a
2152            // real PlPgSqlBlock so the engine can EXECUTE it at
2153            // top level instead of silently swallowing. Pre-
2154            // v7.16.2 the parser threw the body away and the
2155            // engine returned CommandOk for the entire DO; that
2156            // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2157            // $$` into a SEV-1 silent no-op (the IF + the rename
2158            // were both invisible — mailrs's migrate-042 didn't
2159            // actually run). Now the body parses + executes;
2160            // EmbeddedSql inside the block runs immediately
2161            // against the engine (not deferred — we're at top
2162            // level, not inside a trigger row-write loop).
2163            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2164                self.advance();
2165                let body_text = match self.advance() {
2166                    Token::String(s) => s,
2167                    other => {
2168                        return Err(self.err(alloc::format!(
2169                            "expected dollar-quoted body after DO, got {other:?}"
2170                        )));
2171                    }
2172                };
2173                // Optional `LANGUAGE <name>` trailer (idents only).
2174                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2175                    self.advance();
2176                    let _ = self.expect_ident_like()?;
2177                }
2178                // Parse the body — same shape CREATE FUNCTION
2179                // uses for trigger function bodies. If the body
2180                // doesn't parse cleanly we surface the error
2181                // (better than silent no-op).
2182                let block = parse_plpgsql_body(&body_text)?;
2183                Ok(Statement::DoBlock(block))
2184            }
2185            // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2186            // WITH isn't a reserved token in our lexer — comes through
2187            // as `Token::Ident("with")` (case-insensitive).
2188            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2189                self.advance();
2190                self.parse_with_cte_then_select()
2191            }
2192            // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2193            // an identifier — not a reserved keyword.
2194            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2195                self.advance();
2196                let mut analyze = false;
2197                let mut suggest = false;
2198                let mut costs_off = false;
2199                let mut buffers = false;
2200                let mut timing_off = false;
2201                let mut settings = false;
2202                let mut wal = false;
2203                let mut summary_off = false;
2204                let mut format = crate::ast::ExplainFormat::Text;
2205                // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2206                // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2207                // options are comma-separated. Booleans default to ON
2208                // when the value token is omitted (matches PG).
2209                if matches!(self.peek(), Token::LParen) {
2210                    self.advance();
2211                    loop {
2212                        let opt = match self.peek().clone() {
2213                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2214                            other => {
2215                                return Err(self.err(format!(
2216                                    "expected option keyword inside EXPLAIN (…), got {other:?}"
2217                                )));
2218                            }
2219                        };
2220                        self.advance();
2221                        if opt.eq_ignore_ascii_case("suggest") {
2222                            suggest = true;
2223                            // SUGGEST takes no explicit value today.
2224                        } else if opt.eq_ignore_ascii_case("costs") {
2225                            // PG syntax: `COSTS [ON | OFF]`. Default
2226                            // when value omitted is ON, so plain
2227                            // `COSTS` is a no-op. `COSTS OFF` flips.
2228                            // `ON` lexes to `Token::On` (reserved
2229                            // keyword in JOIN ... ON contexts); accept
2230                            // it alongside the bare Ident form so the
2231                            // grammar matches PG verbatim.
2232                            let value = match self.peek().clone() {
2233                                Token::On => {
2234                                    self.advance();
2235                                    true
2236                                }
2237                                Token::Ident(v) | Token::QuotedIdent(v)
2238                                    if v.eq_ignore_ascii_case("off") =>
2239                                {
2240                                    self.advance();
2241                                    false
2242                                }
2243                                Token::Ident(v) | Token::QuotedIdent(v)
2244                                    if v.eq_ignore_ascii_case("true") =>
2245                                {
2246                                    self.advance();
2247                                    true
2248                                }
2249                                _ => true,
2250                            };
2251                            costs_off = !value;
2252                        } else if opt.eq_ignore_ascii_case("analyze")
2253                            || opt.eq_ignore_ascii_case("analyse")
2254                        {
2255                            // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2256                            // Same default-ON rule as ANALYZE keyword form.
2257                            let value = match self.peek().clone() {
2258                                Token::On => {
2259                                    self.advance();
2260                                    true
2261                                }
2262                                Token::Ident(v) | Token::QuotedIdent(v)
2263                                    if v.eq_ignore_ascii_case("off") =>
2264                                {
2265                                    self.advance();
2266                                    false
2267                                }
2268                                Token::Ident(v) | Token::QuotedIdent(v)
2269                                    if v.eq_ignore_ascii_case("true") =>
2270                                {
2271                                    self.advance();
2272                                    true
2273                                }
2274                                _ => true,
2275                            };
2276                            analyze = value;
2277                        } else if opt.eq_ignore_ascii_case("buffers") {
2278                            // v7.37.22 — `BUFFERS [ON|OFF]`.
2279                            let value = match self.peek().clone() {
2280                                Token::On => {
2281                                    self.advance();
2282                                    true
2283                                }
2284                                Token::Ident(v) | Token::QuotedIdent(v)
2285                                    if v.eq_ignore_ascii_case("off") =>
2286                                {
2287                                    self.advance();
2288                                    false
2289                                }
2290                                Token::Ident(v) | Token::QuotedIdent(v)
2291                                    if v.eq_ignore_ascii_case("true") =>
2292                                {
2293                                    self.advance();
2294                                    true
2295                                }
2296                                _ => true,
2297                            };
2298                            buffers = value;
2299                        } else if opt.eq_ignore_ascii_case("timing") {
2300                            // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2301                            // the measured wall-clock annotation.
2302                            let value = match self.peek().clone() {
2303                                Token::On => {
2304                                    self.advance();
2305                                    true
2306                                }
2307                                Token::Ident(v) | Token::QuotedIdent(v)
2308                                    if v.eq_ignore_ascii_case("off") =>
2309                                {
2310                                    self.advance();
2311                                    false
2312                                }
2313                                Token::Ident(v) | Token::QuotedIdent(v)
2314                                    if v.eq_ignore_ascii_case("true") =>
2315                                {
2316                                    self.advance();
2317                                    true
2318                                }
2319                                _ => true,
2320                            };
2321                            timing_off = !value;
2322                        } else if opt.eq_ignore_ascii_case("settings") {
2323                            settings = true;
2324                        } else if opt.eq_ignore_ascii_case("wal") {
2325                            wal = true;
2326                        } else if opt.eq_ignore_ascii_case("summary") {
2327                            // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2328                            // gates the trailing Planning/Execution Time
2329                            // lines now (was accept-and-no-op).
2330                            let value = match self.peek().clone() {
2331                                Token::On => {
2332                                    self.advance();
2333                                    true
2334                                }
2335                                Token::Ident(v) | Token::QuotedIdent(v)
2336                                    if v.eq_ignore_ascii_case("off") =>
2337                                {
2338                                    self.advance();
2339                                    false
2340                                }
2341                                Token::Ident(v) | Token::QuotedIdent(v)
2342                                    if v.eq_ignore_ascii_case("true") =>
2343                                {
2344                                    self.advance();
2345                                    true
2346                                }
2347                                _ => true,
2348                            };
2349                            summary_off = !value;
2350                        } else if opt.eq_ignore_ascii_case("verbose")
2351                            || opt.eq_ignore_ascii_case("format")
2352                        {
2353                            // v7.37.22 — accept-but-no-op the remaining
2354                            // PG options so EXPLAIN-using clients
2355                            // (pgAdmin / DataGrip) don't see syntax
2356                            // errors. FORMAT takes a value (text /
2357                            // json / yaml / xml); skip the next token
2358                            // if it's an ident.
2359                            if opt.eq_ignore_ascii_case("format") {
2360                                if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2361                                {
2362                                    self.advance();
2363                                    format = match v.to_ascii_lowercase().as_str() {
2364                                        "text" => crate::ast::ExplainFormat::Text,
2365                                        "json" => crate::ast::ExplainFormat::Json,
2366                                        "xml" => crate::ast::ExplainFormat::Xml,
2367                                        "yaml" => crate::ast::ExplainFormat::Yaml,
2368                                        other => {
2369                                            return Err(self.err(format!(
2370                                                "EXPLAIN (FORMAT …): unknown format {other:?}; \
2371                                                 supports text, json, xml, yaml"
2372                                            )));
2373                                        }
2374                                    };
2375                                }
2376                            } else {
2377                                // VERBOSE / SUMMARY take optional ON/OFF;
2378                                // consume if present.
2379                                if matches!(self.peek(), Token::On) {
2380                                    self.advance();
2381                                } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2382                                    self.peek().clone()
2383                                    && (v.eq_ignore_ascii_case("off")
2384                                        || v.eq_ignore_ascii_case("true"))
2385                                {
2386                                    self.advance();
2387                                    let _ = v;
2388                                }
2389                            }
2390                        } else {
2391                            return Err(self.err(format!(
2392                                "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2393                            )));
2394                        }
2395                        if matches!(self.peek(), Token::Comma) {
2396                            self.advance();
2397                            continue;
2398                        }
2399                        break;
2400                    }
2401                    if !matches!(self.peek(), Token::RParen) {
2402                        return Err(self.err(format!(
2403                            "expected ')' after EXPLAIN options, got {:?}",
2404                            self.peek()
2405                        )));
2406                    }
2407                    self.advance();
2408                } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2409                    && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2410                {
2411                    self.advance();
2412                    analyze = true;
2413                }
2414                // v7.39 (round 224) — the body may open with WITH (CTEs);
2415                // route through the same CTE-then-SELECT path the top-level
2416                // WITH statement uses. v7.39 (round 225) — DML bodies parse
2417                // too (PG explains INSERT / UPDATE / DELETE).
2418                let inner = match self.peek().clone() {
2419                    Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2420                        self.advance();
2421                        self.parse_with_cte_then_select()?
2422                    }
2423                    Token::Insert => self.parse_insert_stmt(false)?,
2424                    Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2425                        self.advance();
2426                        self.parse_update_after_keyword()?
2427                    }
2428                    Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2429                        self.advance();
2430                        self.parse_delete_after_keyword()?
2431                    }
2432                    _ => self.parse_select_stmt()?,
2433                };
2434                if !matches!(
2435                    inner,
2436                    Statement::Select(_)
2437                        | Statement::Insert(_)
2438                        | Statement::Update(_)
2439                        | Statement::Delete(_)
2440                ) {
2441                    return Err(self.err(format!(
2442                        "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2443                    )));
2444                }
2445                Ok(Statement::Explain(crate::ast::ExplainStatement {
2446                    analyze,
2447                    inner: Box::new(inner),
2448                    suggest,
2449                    costs_off,
2450                    buffers,
2451                    timing_off,
2452                    settings,
2453                    wal,
2454                    summary_off,
2455                    format,
2456                }))
2457            }
2458            Token::Create => self.parse_create_stmt(),
2459            Token::Insert => self.parse_insert_stmt(false),
2460            // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2461            // spelling; route to the same handler. DESC is the
2462            // reserved ORDER BY token, so it gets its own arm.
2463            Token::Ident(s)
2464                if s.eq_ignore_ascii_case("describe")
2465                    && matches!(
2466                        self.tokens.get(self.pos + 1),
2467                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2468                    ) =>
2469            {
2470                self.advance();
2471                let table = self.expect_ident_like()?;
2472                Ok(Statement::ShowColumns(table))
2473            }
2474            Token::Desc
2475                if matches!(
2476                    self.tokens.get(self.pos + 1),
2477                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2478                ) =>
2479            {
2480                self.advance();
2481                let table = self.expect_ident_like()?;
2482                Ok(Statement::ShowColumns(table))
2483            }
2484            // `COPY table [(cols)] TO STDOUT` — the export half of
2485            // pg_dump's COPY pair (the FROM stdin half rides the
2486            // embed import path). Options need a format design and
2487            // error honestly.
2488            Token::Ident(s)
2489                if s.eq_ignore_ascii_case("copy")
2490                    && matches!(
2491                        self.tokens.get(self.pos + 1),
2492                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2493                    ) =>
2494            {
2495                self.advance(); // COPY
2496                let table = self.expect_ident_like()?;
2497                let columns = if matches!(self.peek(), Token::LParen) {
2498                    self.advance();
2499                    let mut cols = alloc::vec![self.expect_ident_like()?];
2500                    while matches!(self.peek(), Token::Comma) {
2501                        self.advance();
2502                        cols.push(self.expect_ident_like()?);
2503                    }
2504                    if !matches!(self.peek(), Token::RParen) {
2505                        return Err(self.err(format!(
2506                            "expected ')' after COPY column list, got {:?}",
2507                            self.peek()
2508                        )));
2509                    }
2510                    self.advance();
2511                    Some(cols)
2512                } else {
2513                    None
2514                };
2515                // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2516                // endpoint. (FROM STDIN still rides the wire/import path —
2517                // its data arrives out of band.)
2518                if matches!(self.peek(), Token::From)
2519                    && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2520                {
2521                    self.advance(); // FROM
2522                    let Token::String(path) = self.advance() else {
2523                        unreachable!()
2524                    };
2525                    let options = self.parse_copy_to_options()?;
2526                    return Ok(Statement::CopyFromFile {
2527                        table,
2528                        columns,
2529                        path,
2530                        options,
2531                    });
2532                }
2533                if !matches!(self.peek(), Token::To) {
2534                    return Err(self.err(format!(
2535                        "COPY: only TO STDOUT is supported here (FROM stdin \
2536                         rides the import path); got {:?}",
2537                        self.peek()
2538                    )));
2539                }
2540                self.advance();
2541                if matches!(self.peek(), Token::String(_)) {
2542                    let Token::String(path) = self.advance() else { unreachable!() };
2543                    let options = self.parse_copy_to_options()?;
2544                    return Ok(Statement::CopyToFile {
2545                        table,
2546                        columns,
2547                        query: None,
2548                        path,
2549                        options,
2550                    });
2551                }
2552                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2553                    return Err(self.err(format!(
2554                        "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2555                        self.peek()
2556                    )));
2557                }
2558                self.advance();
2559                let options = self.parse_copy_to_options()?;
2560                Ok(Statement::CopyTo {
2561                    table,
2562                    columns,
2563                    query: None,
2564                    options,
2565                })
2566            }
2567            // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2568            // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2569            // result set is streamed in COPY format (PG's query form).
2570            Token::Ident(s)
2571                if s.eq_ignore_ascii_case("copy")
2572                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2573            {
2574                self.advance(); // COPY
2575                self.advance(); // (
2576                let query = self.parse_select_stmt()?;
2577                if !matches!(self.peek(), Token::RParen) {
2578                    return Err(self.err(format!(
2579                        "expected ')' after COPY query, got {:?}",
2580                        self.peek()
2581                    )));
2582                }
2583                self.advance(); // )
2584                if !matches!(self.peek(), Token::To) {
2585                    return Err(self.err(format!(
2586                        "COPY (query): only TO STDOUT is supported, got {:?}",
2587                        self.peek()
2588                    )));
2589                }
2590                self.advance();
2591                if matches!(self.peek(), Token::String(_)) {
2592                    let Token::String(path) = self.advance() else { unreachable!() };
2593                    let options = self.parse_copy_to_options()?;
2594                    return Ok(Statement::CopyToFile {
2595                        table: String::new(),
2596                        columns: None,
2597                        query: Some(alloc::boxed::Box::new(query)),
2598                        path,
2599                        options,
2600                    });
2601                }
2602                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2603                    return Err(self.err(format!(
2604                        "COPY (query): TO supports STDOUT only, got {:?}",
2605                        self.peek()
2606                    )));
2607                }
2608                self.advance();
2609                let options = self.parse_copy_to_options()?;
2610                Ok(Statement::CopyTo {
2611                    table: String::new(),
2612                    columns: None,
2613                    query: Some(alloc::boxed::Box::new(query)),
2614                    options,
2615                })
2616            }
2617            // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2618            // Shares the INSERT body; the replace flag lowers it
2619            // onto ON CONFLICT DO UPDATE with an empty assignment
2620            // list (engine: replace the whole row).
2621            Token::Ident(s)
2622                if s.eq_ignore_ascii_case("replace")
2623                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2624            {
2625                self.parse_insert_stmt(true)
2626            }
2627            Token::Begin => {
2628                self.advance();
2629                // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2630                // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2631                // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2632                // is consumed first, then the trailing modes — including the
2633                // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2634                // WORK/TRANSACTION). The explicit level, when present, rides the
2635                // statement so `exec_begin` applies it for this transaction.
2636                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2637                {
2638                    self.advance();
2639                }
2640                let iso = self.parse_isolation_level_clauses()?;
2641                Ok(Statement::Begin(iso))
2642            }
2643            // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2644            // for BEGIN. START is contextual in PG too; pattern-match
2645            // on the ident here. Iso clauses are parse-and-ignored,
2646            // same as BEGIN above.
2647            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2648                self.advance();
2649                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2650                {
2651                    return Err(self.err(alloc::format!(
2652                        "expected TRANSACTION after START, got {:?}",
2653                        self.peek()
2654                    )));
2655                }
2656                self.advance();
2657                let iso = self.parse_isolation_level_clauses()?;
2658                Ok(Statement::Begin(iso))
2659            }
2660            Token::Commit => {
2661                self.advance();
2662                // PG: `COMMIT [WORK | TRANSACTION]`.
2663                if let Token::Ident(w) = self.peek()
2664                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2665                {
2666                    self.advance();
2667                }
2668                Ok(Statement::Commit)
2669            }
2670            // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2671            // COMMIT synonym; pgbench's builtin tpcb-like script closes
2672            // every transaction with `END;` and the drop-in aborted on
2673            // it. Only reachable at statement start (CASE … END lives
2674            // inside expressions), so no ambiguity.
2675            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2676                self.advance();
2677                if let Token::Ident(w) = self.peek()
2678                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2679                {
2680                    self.advance();
2681                }
2682                Ok(Statement::Commit)
2683            }
2684            Token::Rollback => {
2685                self.advance();
2686                // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2687                // savepoint without ending the transaction. Bare
2688                // `ROLLBACK` drops the whole TX.
2689                if matches!(self.peek(), Token::To) {
2690                    self.advance();
2691                    if matches!(self.peek(), Token::Savepoint) {
2692                        self.advance();
2693                    }
2694                    let name = self.expect_ident_like()?;
2695                    Ok(Statement::RollbackToSavepoint(name))
2696                } else {
2697                    Ok(Statement::Rollback)
2698                }
2699            }
2700            Token::Savepoint => {
2701                self.advance();
2702                let name = self.expect_ident_like()?;
2703                Ok(Statement::Savepoint(name))
2704            }
2705            Token::Release => {
2706                self.advance();
2707                // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2708                // is optional in standard SQL.
2709                if matches!(self.peek(), Token::Savepoint) {
2710                    self.advance();
2711                }
2712                let name = self.expect_ident_like()?;
2713                Ok(Statement::ReleaseSavepoint(name))
2714            }
2715            Token::Show => {
2716                self.advance();
2717                // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2718                // v6.1.2 promoted TABLES to a reserved keyword (for
2719                // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2720                // arrives as `Token::Tables` rather than a bare ident.
2721                // USERS / COLUMNS remain bare idents.
2722                let target = match self.advance() {
2723                    Token::Tables => "tables".to_string(),
2724                    // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2725                    // keyword token; recognise it as the SHOW CREATE
2726                    // dispatch keyword too.
2727                    Token::Create => "create".to_string(),
2728                    // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2729                    // keyword too; let SHOW INDEX FROM parse.
2730                    Token::Index => "index".to_string(),
2731                    // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2732                    // reserved (used in aggregate function calls);
2733                    // recognise it here so the parser dispatches
2734                    // to ShowParameter("all") — the engine returns
2735                    // the curated parameter inventory.
2736                    Token::All => "all".to_string(),
2737                    Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2738                    other => {
2739                        return Err(self.err(format!(
2740                            "expected SHOW target, got {other:?}"
2741                        )));
2742                    }
2743                };
2744                match target.as_str() {
2745                    "tables" => Ok(Statement::ShowTables),
2746                    "users" => Ok(Statement::ShowUsers),
2747                    // v7.38 轴 4 — `SHOW transaction_isolation`
2748                    // returns the currently-selected isolation level.
2749                    "transaction_isolation" => Ok(Statement::ShowParameter(
2750                        "transaction_isolation".to_string(),
2751                    )),
2752                    // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2753                    // TABLE <t>` returns a 2-column row: (Table,
2754                    // Create Table). mysqldump emits this for every
2755                    // table at scrape time; without it the dump
2756                    // round-trip stalls.
2757                    // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2758                    // FROM <t>` (also spelled `SHOW INDEX` and
2759                    // `SHOW KEYS`). admin / mysqldump probes use
2760                    // it to list per-table indexes.
2761                    "indexes" | "index" | "keys" => {
2762                        if !matches!(self.peek(), Token::From) {
2763                            return Err(self.err(format!(
2764                                "expected FROM after SHOW INDEXES, got {:?}",
2765                                self.peek()
2766                            )));
2767                        }
2768                        self.advance();
2769                        let table = self.expect_ident_like()?;
2770                        Ok(Statement::ShowIndexes(table))
2771                    }
2772                    // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2773                    // `SHOW VARIABLES`. Both return a 2-column row
2774                    // set listing server-side state; clients probe
2775                    // them at connect time.
2776                    "status" => Ok(Statement::ShowStatus),
2777                    "variables" => {
2778                        // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2779                        if matches!(self.peek(), Token::Like) {
2780                            self.advance();
2781                            let pat = match self.advance() {
2782                                Token::String(p) => p,
2783                                other => {
2784                                    return Err(self.err(format!(
2785                                        "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2786                                    )));
2787                                }
2788                            };
2789                            return Ok(Statement::ShowVariablesLike(pat));
2790                        }
2791                        Ok(Statement::ShowVariables)
2792                    }
2793                    // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2794                    "processlist" => Ok(Statement::ShowProcesslist),
2795                    "create" => {
2796                        // SHOW CREATE TABLE / VIEW / DATABASE — only
2797                        // TABLE is supported in v7.17.
2798                        let kind = match self.advance() {
2799                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2800                            Token::Table => "table".to_string(),
2801                            other => {
2802                                return Err(self.err(format!(
2803                                    "expected TABLE after SHOW CREATE, got {other:?}"
2804                                )));
2805                            }
2806                        };
2807                        if !kind.eq_ignore_ascii_case("table") {
2808                            return Err(self.err(format!(
2809                                "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2810                            )));
2811                        }
2812                        let name = self.expect_ident_like()?;
2813                        Ok(Statement::ShowCreateTable(name))
2814                    }
2815                    // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2816                    // (and `SHOW SCHEMAS` alias). The mysql client uses
2817                    // it to populate the database selector at connect
2818                    // time; without it `mysql -p` errors before the
2819                    // first user query.
2820                    "databases" | "schemas" => Ok(Statement::ShowDatabases),
2821                    // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2822                    // keyword on its own; it lands here as a bare
2823                    // ident. Returning all publications + their
2824                    // scope summary.
2825                    "publications" => Ok(Statement::ShowPublications),
2826                    // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2827                    "subscriptions" => Ok(Statement::ShowSubscriptions),
2828                    "columns" => {
2829                        if !matches!(self.peek(), Token::From) {
2830                            return Err(self.err(format!(
2831                                "expected FROM after SHOW COLUMNS, got {:?}",
2832                                self.peek()
2833                            )));
2834                        }
2835                        self.advance();
2836                        let table = self.expect_ident_like()?;
2837                        Ok(Statement::ShowColumns(table))
2838                    }
2839                    // v7.38 轴 4 surface — `SHOW <param>` for any
2840                    // remaining session / preset parameter name
2841                    // (server_version, search_path, client_encoding,
2842                    // …). The engine's ShowParameter handler does the
2843                    // dispatch; unrecognised names error there with
2844                    // a pointer to pg_settings, not at parse time —
2845                    // so a driver that issues `SHOW spam_setting`
2846                    // gets a clear runtime error instead of a
2847                    // confusing "unknown SHOW target".
2848                    other => {
2849                        // v7.38 (read01 P3.20) — a custom namespaced GUC
2850                        // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2851                        // consume the dotted tail so it round-trips with
2852                        // `SET app.foo` / `current_setting('app.foo')`.
2853                        let mut full = other.to_string();
2854                        while matches!(self.peek(), Token::Dot) {
2855                            self.advance();
2856                            let seg = self.expect_ident_like()?;
2857                            full.push('.');
2858                            full.push_str(&seg.to_ascii_lowercase());
2859                        }
2860                        Ok(Statement::ShowParameter(full))
2861                    }
2862                }
2863            }
2864            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2865            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2866            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2867            // arrived as a bare ident; tokenising it dedicatedly
2868            // keeps the dispatch tree small.
2869            Token::Drop => {
2870                self.advance();
2871                match self.peek() {
2872                    // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2873                    // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2874                    // around DROP ROLE cleanup. SPG has no role-owner
2875                    // model, so consume to boundary as a no-op.
2876                    Token::Ident(s) | Token::QuotedIdent(s)
2877                        if s.eq_ignore_ascii_case("owned") =>
2878                    {
2879                        // v7.39 (round 696) — still a no-op (SPG has no
2880                        // role-owner model), but the ROLE is carried out so
2881                        // the engine can refuse one that does not exist,
2882                        // which is what PG18 does.
2883                        self.advance();
2884                        if self.peek_is_by() {
2885                            self.advance();
2886                        }
2887                        let names = self.take_comma_separated_names();
2888                        self.consume_until_statement_boundary();
2889                        Ok(Statement::ValidateOnly {
2890                            kind: crate::ast::ValidateOnlyKind::RoleName,
2891                            names,
2892                        })
2893                    }
2894                    // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
2895                    // It drops only a TEMPORARY table, and name resolution
2896                    // already prefers the session's own, so the keyword is
2897                    // consumed and the ordinary DROP TABLE path runs.
2898                    Token::Ident(s) | Token::QuotedIdent(s)
2899                        if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
2900                    {
2901                        self.advance();
2902                        if !matches!(self.peek(), Token::Table) {
2903                            return Err(self.err(alloc::format!(
2904                                "expected TABLE after DROP TEMPORARY, got {:?}",
2905                                self.peek()
2906                            )));
2907                        }
2908                        self.parse_drop_table_after_keyword()
2909                    }
2910                    Token::Publication => {
2911                        self.advance();
2912                        // v7.39 (round 754, F31-B4) — the round-753
2913                        // audit probe tripped over the missing
2914                        // `IF EXISTS` here (syntax error).
2915                        let if_exists = self.consume_if_exists();
2916                        let name = self.expect_ident_or_string()?;
2917                        Ok(Statement::DropPublication { name, if_exists })
2918                    }
2919                    Token::Subscription => {
2920                        self.advance();
2921                        let if_exists = self.consume_if_exists();
2922                        let name = self.expect_ident_or_string()?;
2923                        Ok(Statement::DropSubscription { name, if_exists })
2924                    }
2925                    Token::Ident(s) | Token::QuotedIdent(s)
2926                        if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
2927                    {
2928                        self.advance();
2929                        // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
2930                        // login user IS a role in PG, and SPG's store holds
2931                        // both. `IF EXISTS` is accepted on either spelling.
2932                        let if_exists = self.consume_if_exists();
2933                        let name = self.expect_ident_or_string()?;
2934                        Ok(Statement::DropUser { name, if_exists })
2935                    }
2936                    // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
2937                    // CREATE DATABASE has parsed since v7.14 and this did
2938                    // not, so `DROP DATABASE IF EXISTS x` — what every
2939                    // teardown script and pg_dumpall preamble opens with —
2940                    // came back as a syntax error, which IF EXISTS cannot
2941                    // soften. The name is carried so the engine can answer
2942                    // the way PG does; PG never lets this succeed on a
2943                    // single-database server, since the name is either
2944                    // unknown ("database … does not exist", or a notice
2945                    // under IF EXISTS) or the one you are connected to
2946                    // ("cannot drop the currently open database").
2947                    Token::Ident(s) | Token::QuotedIdent(s)
2948                        if s.eq_ignore_ascii_case("database") =>
2949                    {
2950                        self.advance();
2951                        let if_exists = self.consume_if_exists();
2952                        let name = self.expect_ident_or_string()?;
2953                        self.consume_until_statement_boundary();
2954                        Ok(Statement::DropDatabase { name, if_exists })
2955                    }
2956                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
2957                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
2958                        self.advance();
2959                        let if_exists = self.consume_if_exists();
2960                        let name = self.expect_ident_like()?;
2961                        // ON <table>
2962                        if !matches!(self.peek(), Token::On) {
2963                            return Err(self.err(alloc::format!(
2964                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
2965                                self.peek()
2966                            )));
2967                        }
2968                        self.advance();
2969                        let table = self.expect_ident_like()?;
2970                        Ok(Statement::DropTrigger {
2971                            name,
2972                            table,
2973                            if_exists,
2974                        })
2975                    }
2976                    // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
2977                    // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
2978                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
2979                        self.advance();
2980                        let if_exists = self.consume_if_exists();
2981                        let name = self.expect_ident_like()?;
2982                        if !matches!(self.peek(), Token::On) {
2983                            return Err(self.err(alloc::format!(
2984                                "expected ON <table> after DROP RULE {name:?}, got {:?}",
2985                                self.peek()
2986                            )));
2987                        }
2988                        self.advance();
2989                        let table = self.expect_ident_like()?;
2990                        // Optional CASCADE / RESTRICT — accepted, no effect.
2991                        self.consume_until_statement_boundary();
2992                        Ok(Statement::DropRule {
2993                            name,
2994                            table,
2995                            if_exists,
2996                        })
2997                    }
2998                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
2999                    // v7.12.4 ignores any optional arg-list (signature-
3000                    // based overload disambiguation lands in v7.12.5+).
3001                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3002                        self.advance();
3003                        let if_exists = self.consume_if_exists();
3004                        let name = self.expect_ident_like()?;
3005                        // v7.39 (read01 round 62) — the argument list identifies
3006                        // WHICH overload to drop, so it is captured, not
3007                        // discarded. `DROP FUNCTION f` (no list) is legal when
3008                        // the name is unambiguous; the engine enforces that.
3009                        let args = if matches!(self.peek(), Token::LParen) {
3010                            Some(self.parse_function_signature_types()?)
3011                        } else {
3012                            None
3013                        };
3014                        // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3015                        // trailer, which `DROP TABLE` and `DROP INDEX` have
3016                        // accepted since v7.14 and this one refused outright.
3017                        // pg_dump writes it, so refusing was a parse error in
3018                        // the middle of a restore. SPG drops the function
3019                        // either way — it tracks no dependents to cascade to —
3020                        // which is the same reading the other two give it.
3021                        self.consume_drop_behaviour();
3022                        Ok(Statement::DropFunction {
3023                            name,
3024                            args,
3025                            if_exists,
3026                        })
3027                    }
3028                    // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3029                    // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3030                    // emit DROP TABLE IF EXISTS at the head of every
3031                    // CREATE TABLE block so re-importing a dump
3032                    // overwrites prior state. SPG accepts and removes
3033                    // matching tables; CASCADE/RESTRICT trailers
3034                    // accepted silently.
3035                    Token::Table => self.parse_drop_table_after_keyword(),
3036                    // v7.14.0 — DROP INDEX [IF EXISTS] name
3037                    // [CASCADE|RESTRICT]. PG / mysqldump emit this
3038                    // for partial-index renames and pgvector
3039                    // migrations. SPG removes the matching index;
3040                    // IF EXISTS makes the drop idempotent.
3041                    Token::Index => {
3042                        self.advance();
3043                        let if_exists = self.consume_if_exists();
3044                        let name = self.expect_ident_like()?;
3045                        if matches!(
3046                            self.peek(),
3047                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3048                                || s.eq_ignore_ascii_case("restrict")
3049                        ) {
3050                            self.advance();
3051                        }
3052                        Ok(Statement::DropIndex { name, if_exists })
3053                    }
3054                    // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3055                    // [CASCADE|RESTRICT]. SPG is single-database;
3056                    // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3057                    // name [, name…] [CASCADE | RESTRICT]. Real
3058                    // unregister (was silent no-op pre-v7.17).
3059                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3060                        self.advance();
3061                        let if_exists = self.consume_if_exists();
3062                        let mut names = vec![self.expect_ident_like()?];
3063                        while matches!(self.peek(), Token::Comma) {
3064                            self.advance();
3065                            names.push(self.expect_ident_like()?);
3066                        }
3067                        if matches!(
3068                            self.peek(),
3069                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3070                                || s.eq_ignore_ascii_case("restrict")
3071                        ) {
3072                            self.advance();
3073                        }
3074                        Ok(Statement::DropSchema { names, if_exists })
3075                    }
3076                    // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3077                    // name [, name…] [CASCADE|RESTRICT].
3078                    Token::Ident(s) | Token::QuotedIdent(s)
3079                        if s.eq_ignore_ascii_case("type") =>
3080                    {
3081                        self.advance();
3082                        let if_exists = self.consume_if_exists();
3083                        let mut names = vec![self.expect_ident_like()?];
3084                        while matches!(self.peek(), Token::Comma) {
3085                            self.advance();
3086                            names.push(self.expect_ident_like()?);
3087                        }
3088                        if matches!(
3089                            self.peek(),
3090                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3091                                || s.eq_ignore_ascii_case("restrict")
3092                        ) {
3093                            self.advance();
3094                        }
3095                        Ok(Statement::DropType { names, if_exists })
3096                    }
3097                    // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3098                    // name [, name…] [CASCADE|RESTRICT].
3099                    Token::Ident(s) | Token::QuotedIdent(s)
3100                        if s.eq_ignore_ascii_case("domain") =>
3101                    {
3102                        self.advance();
3103                        let if_exists = self.consume_if_exists();
3104                        let mut names = vec![self.expect_ident_like()?];
3105                        while matches!(self.peek(), Token::Comma) {
3106                            self.advance();
3107                            names.push(self.expect_ident_like()?);
3108                        }
3109                        if matches!(
3110                            self.peek(),
3111                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3112                                || s.eq_ignore_ascii_case("restrict")
3113                        ) {
3114                            self.advance();
3115                        }
3116                        Ok(Statement::DropDomain { names, if_exists })
3117                    }
3118                    // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3119                    // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3120                    Token::Ident(s) | Token::QuotedIdent(s)
3121                        if s.eq_ignore_ascii_case("materialized") =>
3122                    {
3123                        self.advance();
3124                        let nxt = self.peek().clone();
3125                        if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3126                        {
3127                            return Err(self.err(alloc::format!(
3128                                "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3129                            )));
3130                        }
3131                        self.advance();
3132                        let if_exists = self.consume_if_exists();
3133                        let mut names = vec![self.expect_ident_like()?];
3134                        while matches!(self.peek(), Token::Comma) {
3135                            self.advance();
3136                            names.push(self.expect_ident_like()?);
3137                        }
3138                        if matches!(
3139                            self.peek(),
3140                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3141                                || s.eq_ignore_ascii_case("restrict")
3142                        ) {
3143                            self.advance();
3144                        }
3145                        Ok(Statement::DropMaterializedView { names, if_exists })
3146                    }
3147                    // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3148                    // name [, name…] [CASCADE|RESTRICT].
3149                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3150                        self.advance();
3151                        let if_exists = self.consume_if_exists();
3152                        let mut names = vec![self.expect_ident_like()?];
3153                        while matches!(self.peek(), Token::Comma) {
3154                            self.advance();
3155                            names.push(self.expect_ident_like()?);
3156                        }
3157                        if matches!(
3158                            self.peek(),
3159                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3160                                || s.eq_ignore_ascii_case("restrict")
3161                        ) {
3162                            self.advance();
3163                        }
3164                        Ok(Statement::DropView { names, if_exists })
3165                    }
3166                    // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3167                    // [CASCADE|RESTRICT]. Real removal from catalog
3168                    // (was a silent no-op pre-v7.17).
3169                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3170                        self.advance();
3171                        let if_exists = self.consume_if_exists();
3172                        let mut names = vec![self.expect_ident_like()?];
3173                        while matches!(self.peek(), Token::Comma) {
3174                            self.advance();
3175                            names.push(self.expect_ident_like()?);
3176                        }
3177                        if matches!(
3178                            self.peek(),
3179                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3180                                || s.eq_ignore_ascii_case("restrict")
3181                        ) {
3182                            self.advance();
3183                        }
3184                        Ok(Statement::DropSequence { names, if_exists })
3185                    }
3186                    // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3187                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3188                        self.advance();
3189                        self.parse_drop_policy_after_keyword()
3190                    }
3191                    // v7.37.17 (17.6 siblings) — DROP <target> for
3192                    // targets SPG doesn't natively track. pg_dump
3193                    // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3194                    // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3195                    // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3196                    // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3197                    // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3198                    // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3199                    // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3200                    // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3201                    // etc. — accept + Empty-return so pg_dump tails
3202                    // load through. Materialized-view drop dispatches
3203                    // to the existing DropTable path when the token
3204                    // is Materialized-View-shaped (elsewhere in
3205                    // this parser).
3206                    Token::Ident(s) | Token::QuotedIdent(s)
3207                        if s.eq_ignore_ascii_case("text")
3208                            // The DROP dispatch matches on PEEK — `text` is
3209                            // not yet consumed, so SEARCH/CONFIGURATION sit
3210                            // at pos+1/pos+2 (the round-695 trap's mirror).
3211                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3212                            && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3213                    {
3214                        // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3215                        // validates the name; DICTIONARY / PARSER / TEMPLATE
3216                        // stay in the noise arm below.
3217                        self.advance(); // TEXT
3218                        self.advance(); // SEARCH
3219                        self.advance(); // CONFIGURATION
3220                        let if_exists = self.consume_if_exists();
3221                        let names = self.take_comma_separated_names();
3222                        self.consume_until_statement_boundary();
3223                        if if_exists {
3224                            return Ok(Statement::Empty);
3225                        }
3226                        Ok(Statement::ValidateOnly {
3227                            kind: crate::ast::ValidateOnlyKind::TsConfigName,
3228                            names,
3229                        })
3230                    }
3231                    Token::Ident(s) | Token::QuotedIdent(s)
3232                        if matches!(
3233                            s.to_ascii_lowercase().as_str(),
3234                            "type"
3235                                | "domain"
3236                                | "operator"
3237                                | "cast"
3238                                // `text` = TEXT SEARCH DICTIONARY / PARSER /
3239                                // TEMPLATE (CONFIGURATION intercepted above).
3240                                | "text"
3241                                | "materialized"
3242                                | "large"
3243                                | "role"
3244                                | "access"
3245                                | "procedure"
3246                                | "routine"
3247                        ) =>
3248                    {
3249                        self.consume_until_statement_boundary();
3250                        Ok(Statement::Empty)
3251                    }
3252                    // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3253                    // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3254                    // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3255                    // foreign-data warning family (round 706) so a
3256                    // CREATE→DROP sequence in a dump stays consistent.
3257                    Token::Ident(s) | Token::QuotedIdent(s)
3258                        if s.eq_ignore_ascii_case("server")
3259                            || s.eq_ignore_ascii_case("foreign") =>
3260                    {
3261                        self.advance();
3262                        self.consume_until_statement_boundary();
3263                        Ok(Statement::ValidateOnly {
3264                            kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3265                            names: Vec::new(),
3266                        })
3267                    }
3268                    Token::Ident(s) | Token::QuotedIdent(s)
3269                        if s.eq_ignore_ascii_case("collation")
3270                            || s.eq_ignore_ascii_case("tablespace") =>
3271                    {
3272                        let kind = if s.eq_ignore_ascii_case("collation") {
3273                            crate::ast::ValidateOnlyKind::CollationName
3274                        } else {
3275                            crate::ast::ValidateOnlyKind::TablespaceName
3276                        };
3277                        self.advance();
3278                        let if_exists = self.consume_if_exists();
3279                        let names = self.take_comma_separated_names();
3280                        self.consume_until_statement_boundary();
3281                        if if_exists {
3282                            return Ok(Statement::Empty);
3283                        }
3284                        Ok(Statement::ValidateOnly { kind, names })
3285                    }
3286                    Token::Ident(s) | Token::QuotedIdent(s)
3287                        if s.eq_ignore_ascii_case("event") =>
3288                    {
3289                        self.advance();
3290                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3291                        {
3292                            self.advance();
3293                        }
3294                        let if_exists = self.consume_if_exists();
3295                        let names = self.take_comma_separated_names();
3296                        self.consume_until_statement_boundary();
3297                        if if_exists {
3298                            return Ok(Statement::Empty);
3299                        }
3300                        Ok(Statement::ValidateOnly {
3301                            kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3302                            names,
3303                        })
3304                    }
3305                    // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3306                    // leave the noise list; see the ValidateOnly kinds.
3307                    Token::Ident(s) | Token::QuotedIdent(s)
3308                        if s.eq_ignore_ascii_case("conversion")
3309                            || s.eq_ignore_ascii_case("language")
3310                            // `DROP PROCEDURAL LANGUAGE` puts the modifier
3311                            // FIRST — the first draft looked for it after.
3312                            || s.eq_ignore_ascii_case("procedural") =>
3313                    {
3314                        let kind = if s.eq_ignore_ascii_case("conversion") {
3315                            crate::ast::ValidateOnlyKind::ConversionName
3316                        } else {
3317                            crate::ast::ValidateOnlyKind::LanguageName
3318                        };
3319                        self.advance();
3320                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3321                        {
3322                            self.advance();
3323                        }
3324                        let if_exists = self.consume_if_exists();
3325                        let names = self.take_comma_separated_names();
3326                        self.consume_until_statement_boundary();
3327                        if if_exists {
3328                            return Ok(Statement::Empty);
3329                        }
3330                        Ok(Statement::ValidateOnly { kind, names })
3331                    }
3332                    // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3333                    // name(argtypes)[, …]`. Parsed for real so the engine
3334                    // can answer as PG does; see Statement::DropAggregate.
3335                    Token::Ident(s) | Token::QuotedIdent(s)
3336                        if s.eq_ignore_ascii_case("aggregate") =>
3337                    {
3338                        self.advance();
3339                        let if_exists = self.consume_if_exists();
3340                        let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3341                        loop {
3342                            let name = self.expect_ident_like()?;
3343                            if !matches!(self.peek(), Token::LParen) {
3344                                return Err(self.err(alloc::format!(
3345                                    "expected argument list after DROP AGGREGATE {name}"
3346                                )));
3347                            }
3348                            self.advance();
3349                            let mut args: Vec<String> = Vec::new();
3350                            let mut star = false;
3351                            loop {
3352                                match self.peek().clone() {
3353                                    Token::RParen => {
3354                                        self.advance();
3355                                        break;
3356                                    }
3357                                    Token::Star => {
3358                                        self.advance();
3359                                        star = true;
3360                                    }
3361                                    Token::Comma => {
3362                                        self.advance();
3363                                    }
3364                                    _ => {
3365                                        // A type name may be multi-token
3366                                        // (`double precision`); glue idents
3367                                        // until , or ).
3368                                        let mut t = self.expect_ident_like()?;
3369                                        while let Token::Ident(nx) = self.peek() {
3370                                            let nx = nx.clone();
3371                                            self.advance();
3372                                            t.push(' ');
3373                                            t.push_str(&nx);
3374                                        }
3375                                        args.push(t);
3376                                    }
3377                                }
3378                            }
3379                            items.push((name, if star { None } else { Some(args) }));
3380                            if matches!(self.peek(), Token::Comma) {
3381                                self.advance();
3382                            } else {
3383                                break;
3384                            }
3385                        }
3386                        self.consume_until_statement_boundary();
3387                        Ok(Statement::DropAggregate { if_exists, items })
3388                    }
3389                    // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3390                    // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3391                    // installed; `IF EXISTS` is the spelling that says do
3392                    // not, and it keeps the no-op.
3393                    Token::Ident(s) | Token::QuotedIdent(s)
3394                        if s.eq_ignore_ascii_case("extension") =>
3395                    {
3396                        self.advance();
3397                        let if_exists = self.consume_if_exists();
3398                        let names = self.take_comma_separated_names();
3399                        self.consume_until_statement_boundary();
3400                        if if_exists {
3401                            return Ok(Statement::Empty);
3402                        }
3403                        Ok(Statement::ValidateOnly {
3404                            kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3405                            names,
3406                        })
3407                    }
3408                    Token::Ident(s) | Token::QuotedIdent(s)
3409                        if s.eq_ignore_ascii_case("statistics") =>
3410                    {
3411                        self.parse_drop_statistics_after_drop()
3412                    }
3413                    other => Err(self.err(format!(
3414                        "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3415                         SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3416                    ))),
3417                }
3418            }
3419            // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3420            // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3421            // and accepted before the view name. SPG materialised
3422            // views re-evaluate on read (always-fresh semantics), so
3423            // the CONCURRENTLY-vs-serial distinction has no runtime
3424            // effect — the refresh body does not block readers either
3425            // way. Same accept-and-no-op pattern as DETACH PARTITION
3426            // CONCURRENTLY (16.5).
3427            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3428                self.advance();
3429                let nxt = self.peek().clone();
3430                if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3431                {
3432                    return Err(self.err(alloc::format!(
3433                        "expected MATERIALIZED after REFRESH, got {nxt:?}"
3434                    )));
3435                }
3436                self.advance();
3437                let nxt2 = self.peek().clone();
3438                if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3439                {
3440                    return Err(self.err(alloc::format!(
3441                        "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3442                    )));
3443                }
3444                self.advance();
3445                // Optional CONCURRENTLY noise word — consumed without
3446                // changing semantics.
3447                if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3448                {
3449                    self.advance();
3450                }
3451                let name = self.expect_ident_like()?;
3452                let with_data = self.parse_optional_with_data(true)?;
3453                Ok(Statement::RefreshMaterializedView { name, with_data })
3454            }
3455            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3456                self.advance();
3457                self.parse_update_after_keyword()
3458            }
3459            // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3460            // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3461            // [CASCADE | RESTRICT]. Clears every row from each named
3462            // table. Parses at the top level; the engine dispatcher
3463            // walks Statement::Truncate.
3464            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3465                self.advance();
3466                // Optional TABLE noise word — PG accepts both the reserved
3467                // token and the bare identifier spelling.
3468                if matches!(self.peek(), Token::Table)
3469                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3470                {
3471                    self.advance();
3472                }
3473                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3474                // not absorbed. The lookahead keeps a table genuinely
3475                // called `only` working: the keyword is a keyword only
3476                // when a name follows it.
3477                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3478                    if s.eq_ignore_ascii_case("only"))
3479                    && matches!(
3480                        self.tokens.get(self.pos + 1),
3481                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3482                    );
3483                if only {
3484                    self.advance();
3485                }
3486                // Table names (comma-separated).
3487                let mut tables = Vec::new();
3488                loop {
3489                    tables.push(self.expect_ident_like()?);
3490                    if matches!(self.peek(), Token::Comma) {
3491                        self.advance();
3492                        continue;
3493                    }
3494                    break;
3495                }
3496                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3497                let mut restart_identity = false;
3498                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3499                {
3500                    self.advance();
3501                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3502                    {
3503                        self.advance();
3504                        restart_identity = true;
3505                    }
3506                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3507                {
3508                    self.advance();
3509                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3510                    {
3511                        self.advance();
3512                    }
3513                }
3514                // Optional CASCADE / RESTRICT.
3515                let mut cascade = false;
3516                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3517                {
3518                    self.advance();
3519                    cascade = true;
3520                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3521                {
3522                    self.advance();
3523                }
3524                Ok(Statement::Truncate {
3525                    tables,
3526                    restart_identity,
3527                    cascade,
3528                    only,
3529                })
3530            }
3531            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3532            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3533            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3534            // rows change so the index tree is always up-to-date;
3535            // REINDEX is a strict no-op. Accept the whole statement
3536            // shape to boundary for pg_dump round-trip compatibility.
3537            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3538                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3539                // index bloat to rebuild, so the work stays a no-op, but PG
3540                // validates what it was pointed at and this swallowed the
3541                // name at parse time — `REINDEX TABLE typo` reported
3542                // success. Measured on PG18: INDEX / TABLE name a relation,
3543                // SCHEMA a schema, SYSTEM nothing.
3544                self.advance();
3545                self.parse_reindex_tail()
3546            }
3547            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3548            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3549            // SPG has no MVCC bloat today (Phase D visibility map
3550            // queues with v7.38); the freezer collapses hot-tier
3551            // rows into cold segments automatically. VACUUM is a
3552            // no-op — pg_dump maintenance scripts and Discourse's
3553            // periodic-maintenance path both emit it.
3554            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3555            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3556            // actual bloat, so the pre-MVCC accept-and-ignore posture
3557            // became a silent no-op on a customer's manual reclaim.
3558            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3559            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3560            // ANALYZE is captured, the optional table name is captured.
3561            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3562                self.advance();
3563                // Parenthesised option list: absorb it.
3564                if matches!(self.peek(), Token::LParen) {
3565                    let mut depth = 0usize;
3566                    loop {
3567                        match self.advance() {
3568                            Token::LParen => depth += 1,
3569                            Token::RParen => {
3570                                depth -= 1;
3571                                if depth == 0 {
3572                                    break;
3573                                }
3574                            }
3575                            Token::Eof => break,
3576                            _ => {}
3577                        }
3578                    }
3579                }
3580                let mut analyze = false;
3581                let mut table: Option<String> = None;
3582                loop {
3583                    match self.peek() {
3584                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3585                        // an identifier, so the loop below broke out on it and
3586                        // dropped the table name: `VACUUM FULL nosuch` was
3587                        // accepted where `VACUUM nosuch` was refused.
3588                        Token::Full => {
3589                            self.advance();
3590                        }
3591                        Token::Ident(w) | Token::QuotedIdent(w) => {
3592                            let wl = w.to_ascii_lowercase();
3593                            match wl.as_str() {
3594                                "full" | "freeze" | "verbose" => {
3595                                    self.advance();
3596                                }
3597                                "analyze" | "analyse" => {
3598                                    analyze = true;
3599                                    self.advance();
3600                                }
3601                                _ => {
3602                                    table = Some(self.expect_ident_like()?);
3603                                    break;
3604                                }
3605                            }
3606                        }
3607                        _ => break,
3608                    }
3609                }
3610                // Optional trailing column list / anything else to the
3611                // statement boundary (PG accepts per-column ANALYZE).
3612                self.consume_until_statement_boundary();
3613                Ok(Statement::Vacuum { table, analyze })
3614            }
3615            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3616            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3617            // <index>. PG stores rows in physical order matching
3618            // an index; SPG's hot-tier is append-only + cold-tier
3619            // is segment-frozen, so clustering has no persistent
3620            // effect. Accept-and-no-op for pg_dump compat.
3621            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3622                // v7.39 (round 535) — same as REINDEX above: the relation is
3623                // carried so the engine can refuse one that does not exist.
3624                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3625                self.advance();
3626                self.parse_cluster_tail()
3627            }
3628            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3629            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3630            // optional string payload; UNLISTEN takes a channel or `*`.
3631            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3632                self.advance();
3633                let ch = match self.advance() {
3634                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3635                    other => {
3636                        return Err(self.err(format!(
3637                            "expected channel name after LISTEN, got {other:?}"
3638                        )));
3639                    }
3640                };
3641                Ok(Statement::Listen(ch))
3642            }
3643            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3644                self.advance();
3645                let channel = match self.advance() {
3646                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3647                    other => {
3648                        return Err(self.err(format!(
3649                            "expected channel name after NOTIFY, got {other:?}"
3650                        )));
3651                    }
3652                };
3653                let payload = if matches!(self.peek(), Token::Comma) {
3654                    self.advance();
3655                    match self.advance() {
3656                        Token::String(p) => Some(p),
3657                        other => {
3658                            return Err(self.err(format!(
3659                                "expected string payload after NOTIFY <channel>, got {other:?}"
3660                            )));
3661                        }
3662                    }
3663                } else {
3664                    None
3665                };
3666                Ok(Statement::Notify { channel, payload })
3667            }
3668            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3669                self.advance();
3670                match self.advance() {
3671                    Token::Star => Ok(Statement::Unlisten(None)),
3672                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3673                    other => Err(self.err(format!(
3674                        "expected channel name or * after UNLISTEN, got {other:?}"
3675                    ))),
3676                }
3677            }
3678            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3679            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3680            // process-wide write lock today; explicit LOCK has no
3681            // effect. Accept-and-no-op for pg_dump / migration
3682            // compat.
3683            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3684                self.advance();
3685                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3686                // engine holds a process-wide write lock), but the TABLE
3687                // NAME is now carried out so the engine can refuse one that
3688                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3689                // READ|WRITE` is a different statement with the same first
3690                // word; it keeps the old no-op, because a MySQL dump's
3691                // bracket names tables it is about to create.
3692                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3693                    if k.eq_ignore_ascii_case("tables"));
3694                if mysql_tables {
3695                    self.consume_until_statement_boundary();
3696                    return Ok(Statement::Empty);
3697                }
3698                if matches!(self.peek(), Token::Table) {
3699                    self.advance();
3700                }
3701                let names = self.take_comma_separated_names();
3702                self.consume_until_statement_boundary();
3703                Ok(Statement::ValidateOnly {
3704                    kind: crate::ast::ValidateOnlyKind::LockTable,
3705                    names,
3706                })
3707            }
3708            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3709            // durability marker + snapshot in PG. SPG has WAL
3710            // checkpointing on a byte / time schedule (v7.37.10
3711            // 60s / 4 MiB defaults). The bare statement parses to
3712            // `Statement::Empty` here (the no_std engine owns no
3713            // WAL / snapshot); v7.37 Epic Du wires the HOST
3714            // (embedded `Database::execute_buffered`, via
3715            // `sql_is_checkpoint`) to force an immediate synchronous
3716            // checkpoint through `Database::checkpoint` — a real
3717            // durability barrier, matching PG.
3718            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3719                self.advance();
3720                self.consume_until_statement_boundary();
3721                Ok(Statement::Empty)
3722            }
3723            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3724                self.advance();
3725                self.parse_delete_after_keyword()
3726            }
3727            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3728            // ALTER is not a reserved keyword in the lexer — handled
3729            // as a bare ident here.
3730            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3731                self.advance();
3732                self.parse_alter_after_keyword()
3733            }
3734            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3735            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3736            // additions needed.
3737            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3738                self.advance();
3739                self.parse_wait_after_keyword()
3740            }
3741            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3742            // Bare ANALYZE → analyse every user table; ANALYZE
3743            // <name> → re-stats one. The argument is an optional
3744            // ident (or quoted ident); anything else is a parse
3745            // error.
3746            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3747            // `WHERE` filter (carved out per V6_7_DESIGN.md
3748            // STABILITY). Lex order: identifier "compact" → "cold"
3749            // → "segments". Anything else after `COMPACT` is a
3750            // parse error.
3751            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3752                self.advance();
3753                let next = self.peek().clone();
3754                let cold = match next {
3755                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3756                    _ => {
3757                        return Err(
3758                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3759                        );
3760                    }
3761                };
3762                if !cold.eq_ignore_ascii_case("cold") {
3763                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3764                }
3765                self.advance();
3766                let next = self.peek().clone();
3767                let segments = match next {
3768                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3769                    _ => {
3770                        return Err(self.err(format!(
3771                            "expected SEGMENTS after COMPACT COLD, got {:?}",
3772                            self.peek()
3773                        )));
3774                    }
3775                };
3776                if !segments.eq_ignore_ascii_case("segments") {
3777                    return Err(self.err(format!(
3778                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3779                    )));
3780                }
3781                self.advance();
3782                Ok(Statement::CompactColdSegments)
3783            }
3784            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3785            // Parsed as a case-insensitive identifier since MERGE
3786            // isn't a reserved lexer keyword (collides with the
3787            // mysqldump `ALGORITHM = MERGE` view clause if it
3788            // were); the inner parser drives the rest of the
3789            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3790            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3791                self.advance();
3792                self.parse_merge_after_keyword()
3793            }
3794            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3795                self.advance();
3796                let target = match self.peek() {
3797                    Token::Eof | Token::Semicolon => None,
3798                    Token::Ident(_) | Token::QuotedIdent(_) => {
3799                        Some(self.expect_ident_like()?)
3800                    }
3801                    other => {
3802                        return Err(self.err(format!(
3803                            "expected table name or end of statement after ANALYZE, got {other:?}"
3804                        )));
3805                    }
3806                };
3807                // v7.39 (round 776, F31 J7) — the per-column form
3808                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3809                // here while the VACUUM arm already consumed it; SPG
3810                // analyzes whole tables, so the list parses and is
3811                // accepted like the VACUUM path's.
3812                if target.is_some() && matches!(self.peek(), Token::LParen) {
3813                    self.advance();
3814                    loop {
3815                        let _ = self.expect_ident_like()?;
3816                        match self.peek() {
3817                            Token::Comma => {
3818                                self.advance();
3819                            }
3820                            Token::RParen => {
3821                                self.advance();
3822                                break;
3823                            }
3824                            other => {
3825                                return Err(self.err(format!(
3826                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
3827                                )));
3828                            }
3829                        }
3830                    }
3831                }
3832                Ok(Statement::Analyze(target))
3833            }
3834            // v7.12.1 — `SET <name> [TO|=] <value>`. The
3835            // `default_text_search_config` parameter is consumed
3836            // by the FTS function dispatcher; other parameter
3837            // names are recorded but treated as a no-op so PG
3838            // dump output loads.
3839            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3840                self.advance();
3841                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3842                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3843                // …` which the SessionVar path handles). `LOCAL` is the only
3844                // one that changes semantics — it scopes the change to the
3845                // current transaction — so capture it; SESSION / GLOBAL are
3846                // accepted and treated as the default session scope.
3847                let mut set_local = false;
3848                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3849                    let q = s.to_ascii_lowercase();
3850                    if q == "local" || q == "session" || q == "global" {
3851                        set_local = q == "local";
3852                        self.advance();
3853                    }
3854                }
3855                // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
3856                // { DEFAULT | <role> }`. pg_dump's ACL section switches
3857                // to the object owner with it. SPG maps it onto the
3858                // session-role machinery (recorded delta: PG moves
3859                // session_user too; SPG moves the effective role).
3860                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3861                    if s.eq_ignore_ascii_case("authorization"))
3862                {
3863                    self.advance(); // AUTHORIZATION
3864                    let role = match self.peek().clone() {
3865                        Token::Default => {
3866                            self.advance();
3867                            None
3868                        }
3869                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3870                            self.advance();
3871                            Some(s)
3872                        }
3873                        _ => None,
3874                    };
3875                    return Ok(Statement::SetRole(role));
3876                }
3877                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3878                // <collation>]` — change the connection client
3879                // charset. SPG stores UTF-8 always and orders
3880                // bytewise; accept as a no-op.
3881                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3882                {
3883                    self.advance();
3884                    // Charset ident-or-string.
3885                    if matches!(
3886                        self.peek(),
3887                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3888                    ) {
3889                        self.advance();
3890                    }
3891                    // Optional `COLLATE <name>`.
3892                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
3893                    {
3894                        self.advance();
3895                        if matches!(
3896                            self.peek(),
3897                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3898                        ) {
3899                            self.advance();
3900                        }
3901                    }
3902                    return Ok(Statement::Empty);
3903                }
3904                // v7.37.17 (17.6 sibling) — PG `SET ROLE
3905                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
3906                // uses this to switch to the object owner before
3907                // recreating tables. SPG has no role system so this
3908                // is a no-op.
3909                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
3910                {
3911                    self.advance(); // ROLE
3912                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
3913                    // reset to the login identity; a name / string sets the
3914                    // effective role that drives current_user + RLS.
3915                    let role = match self.peek().clone() {
3916                        Token::Default => {
3917                            self.advance();
3918                            None
3919                        }
3920                        Token::Ident(s) | Token::QuotedIdent(s)
3921                            if s.eq_ignore_ascii_case("none") =>
3922                        {
3923                            self.advance();
3924                            None
3925                        }
3926                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3927                            self.advance();
3928                            Some(s)
3929                        }
3930                        _ => None,
3931                    };
3932                    return Ok(Statement::SetRole(role));
3933                }
3934                // v7.37.17 (17.6 sibling) — PG `SET SESSION
3935                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
3936                // ISO SQL surface). pg_dump prepends this to fix
3937                // the isolation level for the restore session. SPG
3938                // defaults to READ COMMITTED and doesn't yet honor
3939                // session-set isolation across statements — accept
3940                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
3941                // per-tx form is handled elsewhere.
3942                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
3943                {
3944                    self.advance(); // CHARACTERISTICS
3945                    self.consume_until_statement_boundary();
3946                    return Ok(Statement::Empty);
3947                }
3948                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
3949                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
3950                // pg_dump emits this to control the deferrability of
3951                // FK / UNIQUE constraints across a bulk restore. SPG
3952                // has no deferrable-constraint machinery today; the
3953                // FK checker is strict-immediate. Accept-and-no-op
3954                // for pg_dump round-trip compatibility.
3955                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
3956                {
3957                    self.advance(); // CONSTRAINTS
3958                    // v7.39 (round 288) — no longer a no-op: the trailing
3959                    // DEFERRED / IMMEDIATE sets the transaction's timing.
3960                    // v7.39 (round 308, V29) — and the names are kept.
3961                    // They used to be skipped over on the way to the
3962                    // DEFERRED keyword, so a named form silently behaved
3963                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
3964                    // every deferrable constraint in the transaction.
3965                    let mut names: alloc::vec::Vec<alloc::string::String> =
3966                        alloc::vec::Vec::new();
3967                    if matches!(self.peek(), Token::All) {
3968                        self.advance();
3969                    } else {
3970                        loop {
3971                            let mut n = self.expect_ident_like()?;
3972                            // A schema-qualified name (`public.fk_a`)
3973                            // identifies the same constraint; PG resolves
3974                            // it by the trailing segment.
3975                            while matches!(self.peek(), Token::Dot) {
3976                                self.advance();
3977                                n = self.expect_ident_like()?;
3978                            }
3979                            names.push(n);
3980                            if matches!(self.peek(), Token::Comma) {
3981                                self.advance();
3982                            } else {
3983                                break;
3984                            }
3985                        }
3986                    }
3987                    let deferred = match self.peek() {
3988                        Token::Ident(s) | Token::QuotedIdent(s)
3989                            if s.eq_ignore_ascii_case("deferred") =>
3990                        {
3991                            true
3992                        }
3993                        Token::Ident(s) | Token::QuotedIdent(s)
3994                            if s.eq_ignore_ascii_case("immediate") =>
3995                        {
3996                            false
3997                        }
3998                        other => {
3999                            return Err(self.err(alloc::format!(
4000                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4001                            )));
4002                        }
4003                    };
4004                    self.advance();
4005                    return Ok(Statement::SetConstraints { names, deferred });
4006                }
4007                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4008                // { DEFAULT | '<role>' | <ident> }` (mailrs
4009                // round-10 A.1). pg_dump preamble emits the
4010                // `DEFAULT` form to reset session authorization.
4011                //
4012                // v7.39 (round 697) — this said "SPG has no role system so
4013                // this is a strict no-op". SPG has had one since round 58;
4014                // the comment outlived it, and with it the reason a name
4015                // that is not a role was accepted here. It still switches
4016                // no authorization — what it does now is refuse a role
4017                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4018                // AUTHORIZATION` (handled by the RESET parser
4019                // elsewhere). Reference:
4020                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4021                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4022                {
4023                    self.advance(); // AUTHORIZATION
4024                    match self.peek().clone() {
4025                        Token::Default => {
4026                            self.advance();
4027                        }
4028                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4029                            self.advance();
4030                            return Ok(Statement::ValidateOnly {
4031                                kind: crate::ast::ValidateOnlyKind::RoleName,
4032                                names: alloc::vec![r],
4033                            });
4034                        }
4035                        other => {
4036                            return Err(self.err(alloc::format!(
4037                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4038                            )));
4039                        }
4040                    }
4041                    return Ok(Statement::Empty);
4042                }
4043                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4044                // ISOLATION LEVEL { READ COMMITTED | READ
4045                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4046                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4047                // PG-standard surface. v7.37.8 accepts the syntax
4048                // and tracks the selected level on
4049                // `Engine::current_isolation_level()`; the actual
4050                // MVCC / SSI semantics implementation lands in
4051                // the 轴 4 isolation framework (separate train).
4052                // PG itself maps READ UNCOMMITTED to READ COMMITTED
4053                // internally; SPG behaves the same (effectively
4054                // READ COMMITTED at every level today).
4055                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4056                {
4057                    self.advance(); // TRANSACTION
4058                    let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
4059                    return Ok(Statement::SetTransaction { isolation: level });
4060                }
4061                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4062                // alias — same accept-as-no-op as SET NAMES.
4063                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4064                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4065                {
4066                    self.advance(); // CHARACTER
4067                    self.advance(); // SET
4068                    if matches!(
4069                        self.peek(),
4070                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4071                    ) {
4072                        self.advance();
4073                    }
4074                    return Ok(Statement::Empty);
4075                }
4076                // v7.39 (GUC) — PG spells the timezone GUC as two
4077                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4078                // where <value> is a string/ident or the LOCAL /
4079                // DEFAULT keyword (both mean "back to the default").
4080                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4081                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4082                {
4083                    self.advance(); // TIME
4084                    self.advance(); // ZONE
4085                    let value = match self.peek().clone() {
4086                        Token::Ident(s)
4087                            if s.eq_ignore_ascii_case("local")
4088                                || s.eq_ignore_ascii_case("default") =>
4089                        {
4090                            self.advance();
4091                            crate::ast::SetValue::Default
4092                        }
4093                        Token::Default => {
4094                            self.advance();
4095                            crate::ast::SetValue::Default
4096                        }
4097                        _ => self.parse_set_value()?,
4098                    };
4099                    return Ok(Statement::SetParameter {
4100                        name: "timezone".into(),
4101                        value,
4102                        local: set_local,
4103                    });
4104                }
4105                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4106                // MySQL USER-variable assignment: its own per-session
4107                // namespace, an arbitrary expression on the right, and `:=`
4108                // as a second spelling of `=`. It used to fall into the
4109                // session-PARAMETER list below, whose values are literals and
4110                // whose store nothing reads back under a `@` name — so the
4111                // assignment reported success and vanished.
4112                //
4113                // A `@@`-prefixed LHS is a real engine setting and keeps the
4114                // old path.
4115                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4116                    return self.parse_set_user_vars();
4117                }
4118                // v7.14.0 — multi-assignment form
4119                // `SET a = 1, b = 2, …`. Single-assignment is the
4120                // 1-element case. Each LHS may be a regular ident
4121                // or a SessionVar (`@VAR` / `@@VAR`).
4122                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4123                loop {
4124                    let lhs = match self.peek().clone() {
4125                        Token::SessionVar(s) => {
4126                            self.advance();
4127                            s
4128                        }
4129                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4130                        other => {
4131                            return Err(self.err(format!(
4132                                "expected parameter name after SET, got {other:?}"
4133                            )));
4134                        }
4135                    };
4136                    // Accept either `=` or the bare `TO` keyword.
4137                    match self.peek() {
4138                        Token::Eq => {
4139                            self.advance();
4140                        }
4141                        Token::To => {
4142                            self.advance();
4143                        }
4144                        other => {
4145                            return Err(self.err(format!(
4146                                "expected `=` or TO after SET {lhs}, got {other:?}"
4147                            )));
4148                        }
4149                    }
4150                    let mut value = self.parse_set_value()?;
4151                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4152                    // `, name TO` continues a MySQL-style multi-assign,
4153                    // anything else is a PG list VALUE
4154                    // (`SET search_path = myschema, public`) folded into
4155                    // one comma-joined string.
4156                    while matches!(self.peek(), Token::Comma) {
4157                        let is_assign = matches!(
4158                            self.tokens.get(self.pos + 1),
4159                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4160                        ) && matches!(
4161                            self.tokens.get(self.pos + 2),
4162                            Some(Token::Eq | Token::To)
4163                        );
4164                        if is_assign {
4165                            break;
4166                        }
4167                        self.advance(); // comma
4168                        let next = self.parse_set_value()?;
4169                        let joined = alloc::format!(
4170                            "{}, {}",
4171                            set_value_text(&value),
4172                            set_value_text(&next)
4173                        );
4174                        value = crate::ast::SetValue::String(joined);
4175                    }
4176                    pairs.push((lhs, value));
4177                    if matches!(self.peek(), Token::Comma) {
4178                        self.advance();
4179                        continue;
4180                    }
4181                    break;
4182                }
4183                if pairs.len() == 1 {
4184                    let (name, value) = pairs.into_iter().next().unwrap();
4185                    Ok(Statement::SetParameter {
4186                        name,
4187                        value,
4188                        local: set_local,
4189                    })
4190                } else {
4191                    Ok(Statement::SetParameterList(pairs))
4192                }
4193            }
4194            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4195            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4196                self.advance();
4197                match self.peek().clone() {
4198                    Token::All => {
4199                        self.advance();
4200                        Ok(Statement::ResetParameter(None))
4201                    }
4202                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4203                        self.advance();
4204                        Ok(Statement::ResetParameter(None))
4205                    }
4206                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4207                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4208                        self.advance();
4209                        Ok(Statement::SetRole(None))
4210                    }
4211                    // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4212                    // (pg_dump's return from the owner switch).
4213                    Token::Ident(s) | Token::QuotedIdent(s)
4214                        if s.eq_ignore_ascii_case("session")
4215                            && matches!(
4216                                self.tokens.get(self.pos + 1),
4217                                Some(Token::Ident(a) | Token::QuotedIdent(a))
4218                                    if a.eq_ignore_ascii_case("authorization")
4219                            ) =>
4220                    {
4221                        self.advance(); // SESSION
4222                        self.advance(); // AUTHORIZATION
4223                        Ok(Statement::SetRole(None))
4224                    }
4225                    _ => {
4226                        let name = self.parse_set_param_name()?;
4227                        Ok(Statement::ResetParameter(Some(name)))
4228                    }
4229                }
4230            }
4231            // v7.39 (round 218) — server-side cursors.
4232            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4233            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4234            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4235            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4236                self.advance();
4237                match self.peek().clone() {
4238                    Token::All => {
4239                        self.advance();
4240                        Ok(Statement::CloseCursor { name: None })
4241                    }
4242                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4243                        self.advance();
4244                        Ok(Statement::CloseCursor { name: None })
4245                    }
4246                    Token::Ident(n) | Token::QuotedIdent(n) => {
4247                        self.advance();
4248                        Ok(Statement::CloseCursor { name: Some(n) })
4249                    }
4250                    other => Err(self.err(format!(
4251                        "expected cursor name or ALL after CLOSE, got {other:?}"
4252                    ))),
4253                }
4254            }
4255            other => Err(self.err(format!(
4256                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4257                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4258            ))),
4259        }
4260    }
4261
4262    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4263    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4264    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4265    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4266    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4267        self.advance(); // DECLARE
4268        let name = match self.advance() {
4269            Token::Ident(n) | Token::QuotedIdent(n) => n,
4270            other => {
4271                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4272            }
4273        };
4274        let mut scroll: Option<bool> = None;
4275        loop {
4276            match self.peek() {
4277                Token::Ident(s)
4278                    if s.eq_ignore_ascii_case("binary")
4279                        || s.eq_ignore_ascii_case("insensitive")
4280                        || s.eq_ignore_ascii_case("asensitive") =>
4281                {
4282                    self.advance();
4283                }
4284                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4285                    self.advance();
4286                    scroll = Some(true);
4287                }
4288                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4289                {
4290                    self.advance(); // NO
4291                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4292                        return Err(self.err(format!(
4293                            "expected SCROLL after NO in DECLARE, got {:?}",
4294                            self.peek()
4295                        )));
4296                    }
4297                    self.advance();
4298                    scroll = Some(false);
4299                }
4300                _ => break,
4301            }
4302        }
4303        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4304            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4305        }
4306        self.advance();
4307        let mut hold = false;
4308        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4309            self.advance();
4310            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4311                return Err(self.err(format!(
4312                    "expected HOLD after WITH in DECLARE, got {:?}",
4313                    self.peek()
4314                )));
4315            }
4316            self.advance();
4317            hold = true;
4318        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4319            self.advance();
4320            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4321                return Err(self.err(format!(
4322                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4323                    self.peek()
4324                )));
4325            }
4326            self.advance();
4327        }
4328        if !matches!(self.peek(), Token::For) {
4329            return Err(self.err(format!(
4330                "expected FOR before the cursor query, got {:?}",
4331                self.peek()
4332            )));
4333        }
4334        self.advance();
4335        let query = self.parse_one_statement()?;
4336        Ok(Statement::DeclareCursor {
4337            name,
4338            scroll,
4339            hold,
4340            query: alloc::boxed::Box::new(query),
4341        })
4342    }
4343
4344    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4345    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4346    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4347    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4348        use crate::ast::CursorDirection as D;
4349        self.advance(); // FETCH / MOVE
4350        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4351            let neg = if matches!(this.peek(), Token::Minus) {
4352                this.advance();
4353                true
4354            } else {
4355                false
4356            };
4357            match this.advance() {
4358                Token::Integer(v) => Ok(if neg { -v } else { v }),
4359                other => Err(this.err(format!("expected count, got {other:?}"))),
4360            }
4361        };
4362        let direction = match self.peek().clone() {
4363            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4364                self.advance();
4365                D::Next
4366            }
4367            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4368                self.advance();
4369                D::Prior
4370            }
4371            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4372                self.advance();
4373                D::First
4374            }
4375            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4376                self.advance();
4377                D::Last
4378            }
4379            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4380                self.advance();
4381                D::Absolute(signed_count(self)?)
4382            }
4383            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4384                self.advance();
4385                D::Relative(signed_count(self)?)
4386            }
4387            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4388                self.advance();
4389                match self.peek().clone() {
4390                    Token::All => {
4391                        self.advance();
4392                        D::All
4393                    }
4394                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4395                        self.advance();
4396                        D::All
4397                    }
4398                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4399                    _ => D::Next, // bare FORWARD = FORWARD 1
4400                }
4401            }
4402            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4403                self.advance();
4404                match self.peek().clone() {
4405                    Token::All => {
4406                        self.advance();
4407                        D::BackwardAll
4408                    }
4409                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4410                        self.advance();
4411                        D::BackwardAll
4412                    }
4413                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4414                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4415                }
4416            }
4417            Token::All => {
4418                self.advance();
4419                D::All
4420            }
4421            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4422                self.advance();
4423                D::All
4424            }
4425            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4426            // Bare `FETCH <name>` — direction defaults to NEXT.
4427            _ => D::Next,
4428        };
4429        // Optional FROM / IN.
4430        if matches!(self.peek(), Token::From)
4431            || matches!(self.peek(), Token::In)
4432            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4433        {
4434            self.advance();
4435        }
4436        let name = match self.advance() {
4437            Token::Ident(n) | Token::QuotedIdent(n) => n,
4438            other => {
4439                return Err(self.err(format!("expected cursor name, got {other:?}")));
4440            }
4441        };
4442        Ok(if is_move {
4443            Statement::MoveCursor { name, direction }
4444        } else {
4445            Statement::FetchCursor { name, direction }
4446        })
4447    }
4448
4449    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4450    /// [(kind, …)] ON <col>, … FROM <table>`.
4451    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4452        self.advance(); // STATISTICS
4453        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4454        let mut if_not_exists = false;
4455        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4456            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4457        {
4458            self.advance();
4459            self.advance();
4460            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4461                self.advance();
4462                if_not_exists = true;
4463            }
4464        }
4465        let name = self.expect_ident_like()?;
4466        let mut kinds = Vec::new();
4467        if matches!(self.peek(), Token::LParen) {
4468            self.advance();
4469            loop {
4470                let k = self.expect_ident_like()?;
4471                // PG stores the single letters; accept the spelled-out
4472                // names the SQL uses and record what PG records.
4473                kinds.push(match k.to_ascii_lowercase().as_str() {
4474                    "ndistinct" => String::from("d"),
4475                    "dependencies" => String::from("f"),
4476                    "mcv" => String::from("m"),
4477                    other => {
4478                        return Err(
4479                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4480                        );
4481                    }
4482                });
4483                match self.advance() {
4484                    Token::Comma => {}
4485                    Token::RParen => break,
4486                    other => {
4487                        return Err(self.err(alloc::format!(
4488                            "expected ',' or ')' in statistics kind list, got {other:?}"
4489                        )));
4490                    }
4491                }
4492            }
4493        }
4494        if !matches!(self.peek(), Token::On) {
4495            return Err(self.err(alloc::format!(
4496                "expected ON in CREATE STATISTICS, got {:?}",
4497                self.peek()
4498            )));
4499        }
4500        self.advance();
4501        let mut columns = Vec::new();
4502        loop {
4503            columns.push(self.expect_ident_like()?);
4504            if matches!(self.peek(), Token::Comma) {
4505                self.advance();
4506            } else {
4507                break;
4508            }
4509        }
4510        if !matches!(self.peek(), Token::From) {
4511            return Err(self.err(alloc::format!(
4512                "expected FROM in CREATE STATISTICS, got {:?}",
4513                self.peek()
4514            )));
4515        }
4516        self.advance();
4517        let table = self.expect_ident_like()?;
4518        Ok(Statement::CreateStatistics {
4519            name,
4520            if_not_exists,
4521            kinds,
4522            columns,
4523            table,
4524        })
4525    }
4526
4527    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4528    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4529    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4530    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4531    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4532    /// forward call.
4533    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4534        self.advance(); // TABLE
4535        let if_exists = self.consume_if_exists();
4536        let mut names: Vec<String> = Vec::new();
4537        loop {
4538            names.push(self.expect_ident_like()?);
4539            if matches!(self.peek(), Token::Comma) {
4540                self.advance();
4541                continue;
4542            }
4543            break;
4544        }
4545        if matches!(
4546            self.peek(),
4547            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4548                || s.eq_ignore_ascii_case("restrict")
4549        ) {
4550            self.advance();
4551        }
4552        Ok(Statement::DropTable { names, if_exists })
4553    }
4554
4555    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4556        self.advance(); // STATISTICS
4557        let mut if_exists = false;
4558        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4559            && matches!(self.tokens.get(self.pos + 1),
4560                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4561        {
4562            self.advance();
4563            self.advance();
4564            if_exists = true;
4565        }
4566        let name = self.expect_ident_like()?;
4567        Ok(Statement::DropStatistics { name, if_exists })
4568    }
4569
4570    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4571        debug_assert!(matches!(self.peek(), Token::Create));
4572        self.advance();
4573        match self.peek() {
4574            Token::Table => self.parse_create_table_stmt_after_create(),
4575            Token::Index => self.parse_create_index_stmt_after_create(false),
4576            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4577            // object now. It used to be consumed by the CREATE-noise
4578            // arm, so a pg_dump that declares extended statistics
4579            // restored silently without them and reflection showed
4580            // nothing.
4581            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4582                self.parse_create_statistics_after_create()
4583            }
4584            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4585            // The `UNIQUE` modifier turns a partial index into a
4586            // partial-uniqueness invariant (only rows matching the
4587            // WHERE predicate are checked for duplicates). mailrs
4588            // K1 (3 hits: email_templates default, calendar_events
4589            // master, calendar_events instance).
4590            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4591                self.advance();
4592                if !matches!(self.peek(), Token::Index) {
4593                    return Err(self.err(alloc::format!(
4594                        "expected INDEX after CREATE UNIQUE, got {:?}",
4595                        self.peek()
4596                    )));
4597                }
4598                self.parse_create_index_stmt_after_create(true)
4599            }
4600            Token::Publication => {
4601                self.advance();
4602                self.parse_create_publication_after_keyword()
4603            }
4604            Token::Subscription => {
4605                self.advance();
4606                self.parse_create_subscription_after_keyword()
4607            }
4608            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4609            // USER isn't a reserved keyword — we look for the bare
4610            // identifier so the lexer doesn't have to grow a token.
4611            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4612                self.advance();
4613                self.parse_create_user_after_keyword(true)
4614            }
4615            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4616            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4617            // the default of the LOGIN attribute.
4618            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4619                self.advance();
4620                self.parse_create_user_after_keyword(false)
4621            }
4622            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4623            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4624                self.advance();
4625                self.parse_create_policy_after_keyword()
4626            }
4627            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4628            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4629            // no-op. mailrs follow-up F3.
4630            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4631                self.advance();
4632                self.parse_create_extension_after_keyword()
4633            }
4634            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4635            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4636            // optional; absorb it here and forward to the
4637            // per-kind parsers with the flag. OR is a reserved
4638            // keyword token.
4639            Token::Or => {
4640                self.advance();
4641                let next = self.peek();
4642                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4643                    return Err(self.err(alloc::format!(
4644                        "expected REPLACE after CREATE OR, got {next:?}"
4645                    )));
4646                };
4647                if !s2.eq_ignore_ascii_case("replace") {
4648                    return Err(self.err(alloc::format!(
4649                        "expected REPLACE after CREATE OR, got {s2:?}"
4650                    )));
4651                }
4652                self.advance();
4653                self.parse_create_function_or_trigger_after_or_replace(true)
4654            }
4655            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4656                self.advance();
4657                self.parse_create_function_after_keyword(false)
4658            }
4659            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4660                self.advance();
4661                self.parse_create_trigger_after_keyword(false)
4662            }
4663            // v7.39 (round 139) — CREATE RULE …
4664            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4665                self.advance();
4666                self.parse_create_rule_after_keyword(false)
4667            }
4668            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4669            // trigger is a row-level AFTER trigger that additionally carries
4670            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4671            // path already tolerates and skips those clauses, so consuming the
4672            // CONSTRAINT keyword and reusing it makes the statement parse and the
4673            // trigger fire. (The deferral timing itself is not yet honoured —
4674            // SPG fires it as a plain AFTER trigger, which is correct behaviour
4675            // for every non-deferred use.)
4676            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4677                self.advance();
4678                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4679                    if t.eq_ignore_ascii_case("trigger"))
4680                {
4681                    return Err(self.err(alloc::format!(
4682                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4683                        self.peek()
4684                    )));
4685                }
4686                self.advance();
4687                self.parse_create_trigger_after_keyword(false)
4688            }
4689            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4690            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4691                self.advance();
4692                self.parse_create_sequence_after_keyword(false)
4693            }
4694            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4695            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4696                self.advance();
4697                self.parse_create_view_after_keyword(false, false, false)
4698            }
4699            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4700            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4701            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4702            // appear (in any order) between `CREATE` and `VIEW` in
4703            // every mysqldump-emitted view. Pre-2.6 the parser
4704            // rejected the prefix and the customer's whole view
4705            // backup failed on the first view. The hints are pure
4706            // planner / permission metadata; SPG's view-rewrite
4707            // path is semantically equivalent for all three
4708            // algorithms in v7.17 (TEMPTABLE differs only in
4709            // perf for huge views — out of v7.17 scope), and
4710            // DEFINER / SQL SECURITY are pure single-user
4711            // permissioning that SPG ignores by design.
4712            Token::Ident(s) | Token::QuotedIdent(s)
4713                if s.eq_ignore_ascii_case("algorithm")
4714                    || s.eq_ignore_ascii_case("definer")
4715                    || s.eq_ignore_ascii_case("sql") =>
4716            {
4717                self.consume_mysql_view_prefix()?;
4718                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4719                // (in any order, in any combination), the next
4720                // keyword must be VIEW. mysqldump never emits these
4721                // prefixes on non-view statements.
4722                let next = self.peek().clone();
4723                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4724                    if s2.eq_ignore_ascii_case("view"))
4725                {
4726                    self.advance();
4727                    self.parse_create_view_after_keyword(false, false, false)
4728                } else {
4729                    Err(self.err(alloc::format!(
4730                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4731                    )))
4732                }
4733            }
4734            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4735            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4736                self.advance();
4737                self.parse_create_type_after_keyword()
4738            }
4739            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4740            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4741            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4742                self.advance();
4743                self.parse_create_domain_after_keyword()
4744            }
4745            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4746            // name [AUTHORIZATION user]. Real catalog registry
4747            // (was silent-no-op'd pre-v7.17).
4748            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4749                self.advance();
4750                let if_not_exists = self.parse_if_not_exists();
4751                let name = self.expect_ident_like()?;
4752                // Optional `AUTHORIZATION <user>` trailer — accepted,
4753                // ignored (single-user catalog).
4754                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4755                    if s.eq_ignore_ascii_case("authorization"))
4756                {
4757                    self.advance();
4758                    let _ = self.expect_ident_like()?;
4759                }
4760                Ok(Statement::CreateSchema { name, if_not_exists })
4761            }
4762            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4763            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4764                self.advance();
4765                let next = self.peek().clone();
4766                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4767                {
4768                    self.advance();
4769                    self.parse_create_materialized_view_after_keyword()
4770                } else {
4771                    Err(self.err(alloc::format!(
4772                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4773                    )))
4774                }
4775            }
4776            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4777            // no-op below), an UNLOGGED table is a real, fully-usable table in
4778            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4779            // durability optimisation is a follow-up), so a dump / app that
4780            // declares UNLOGGED tables works instead of failing to parse.
4781            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4782                self.advance(); // UNLOGGED
4783                if matches!(self.peek(), Token::Table) {
4784                    self.parse_create_table_stmt_after_create()
4785                } else {
4786                    Err(self.err(format!(
4787                        "expected TABLE after CREATE UNLOGGED, got {:?}",
4788                        self.peek()
4789                    )))
4790                }
4791            }
4792            Token::Ident(s) | Token::QuotedIdent(s)
4793                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4794            {
4795                self.advance();
4796                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4797                let next = self.peek().clone();
4798                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4799                {
4800                    self.advance();
4801                    self.parse_create_sequence_after_keyword(true)
4802                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4803                {
4804                    self.advance();
4805                    self.parse_create_view_after_keyword(false, false, true)
4806                } else {
4807                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4808                    // consumed and answered OK while creating nothing, so
4809                    // every statement that touched the table afterwards failed
4810                    // with "table not found" — the DDL itself lied. It is a
4811                    // real CREATE TABLE now, marked temporary so the executor
4812                    // puts it in the session's own namespace. An optional
4813                    // TABLE keyword may or may not be present (`CREATE TEMP t`
4814                    // is not legal, but the keyword is consumed by the
4815                    // CREATE TABLE parser itself).
4816                    let stmt = self.parse_create_table_stmt_after_create()?;
4817                    match stmt {
4818                        Statement::CreateTable(mut c) => {
4819                            c.temporary = true;
4820                            Ok(Statement::CreateTable(c))
4821                        }
4822                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4823                        // CTAS node, which needs the same session namespace.
4824                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4825                            m.temporary = true;
4826                            Ok(Statement::CreateMaterializedView(m))
4827                        }
4828                        other => Ok(other),
4829                    }
4830                }
4831            }
4832            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4833            // BEGIN <body> END`. The body may reference `@var`
4834            // session variables, SET statements, internal `;`
4835            // terminators, etc. SPG has no procedure runtime, so
4836            // consume the whole `CREATE PROCEDURE … END` block as
4837            // a no-op so mysqldump scripts that include stored
4838            // routines load through. The matching-END consumer
4839            // tracks BEGIN/END nesting depth to handle nested
4840            // BEGIN blocks correctly.
4841            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4842                self.consume_mysql_routine_body();
4843                Ok(Statement::Empty)
4844            }
4845            // v7.14.0 — pg_dump / mysqldump emit
4846            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4847            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4848            // SPG is single-schema / single-database; these have
4849            // no behavioural effect, so consume + return Empty.
4850            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4851            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4852            // moved up to real parser branches. DATABASE / ROLE /
4853            // POLICY / OPERATOR stay no-op forever
4854            // (single-database, hardcoded roles).
4855            Token::Ident(s) | Token::QuotedIdent(s)
4856                if matches!(
4857                    s.to_ascii_lowercase().as_str(),
4858                    "database"
4859                        | "role"
4860                        | "operator"
4861                        | "cast"
4862                        | "aggregate"
4863                        | "language"
4864                        | "collation"
4865                        | "conversion"
4866                        // v7.17.0 Phase 8 (audit N6) — rarely-
4867                        // emitted pg_dump shapes that should
4868                        // load through without a parser error.
4869                        // SPG has no planner statistics catalog,
4870                        // no event-trigger hooks, no foreign-
4871                        // data-wrapper infrastructure; consume
4872                        // + return Empty.
4873                        | "statistics"
4874                        | "event"
4875                        // v7.37.17 (17.6 siblings) — additional CREATE
4876                        // targets pg_dump / operator install scripts
4877                        // may emit that SPG has no matching machinery
4878                        // for. Consume + Empty-return.
4879                        | "text"
4880                        | "tablespace"
4881                        | "access"
4882                        | "large"
4883                ) =>
4884            {
4885                // DATABASE is the one member of this list PG refuses
4886                // inside a transaction block; the rest (ROLE, CAST,
4887                // TABLESPACE, …) it runs there quite happily, so only
4888                // this one is named. Still a no-op otherwise — SPG is
4889                // single-database.
4890                let is_database = s.eq_ignore_ascii_case("database");
4891                self.consume_until_statement_boundary();
4892                if is_database {
4893                    return Ok(Statement::NoOpPreventedInTransaction {
4894                        what: String::from("CREATE DATABASE"),
4895                    });
4896                }
4897                Ok(Statement::Empty)
4898            }
4899            // v7.39 (round 706) — the foreign-data family leaves the silent
4900            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
4901            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
4902            // FDW machinery), but the ENGINE now warns, so a restore log
4903            // says what will not function instead of reporting success.
4904            Token::Ident(s) | Token::QuotedIdent(s)
4905                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
4906            {
4907                self.consume_until_statement_boundary();
4908                Ok(Statement::ValidateOnly {
4909                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
4910                    names: Vec::new(),
4911                })
4912            }
4913            other => Err(self.err(format!(
4914                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
4915            ))),
4916        }
4917    }
4918
4919    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
4920    /// keyword decides whether we parse a function or trigger
4921    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
4922    /// PROCEDURE) — those land in later releases.
4923    fn parse_create_function_or_trigger_after_or_replace(
4924        &mut self,
4925        or_replace: bool,
4926    ) -> Result<Statement, ParseError> {
4927        let tok = self.peek();
4928        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4929            return Err(self.err(alloc::format!(
4930                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
4931            )));
4932        };
4933        if s.eq_ignore_ascii_case("function") {
4934            self.advance();
4935            self.parse_create_function_after_keyword(or_replace)
4936        } else if s.eq_ignore_ascii_case("trigger") {
4937            self.advance();
4938            self.parse_create_trigger_after_keyword(or_replace)
4939        } else if s.eq_ignore_ascii_case("rule") {
4940            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
4941            self.advance();
4942            self.parse_create_rule_after_keyword(or_replace)
4943        } else if s.eq_ignore_ascii_case("view") {
4944            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
4945            self.advance();
4946            self.parse_create_view_after_keyword(or_replace, false, false)
4947        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
4948            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
4949            self.advance();
4950            let nxt = self.peek().clone();
4951            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
4952            {
4953                self.advance();
4954                self.parse_create_view_after_keyword(or_replace, false, true)
4955            } else {
4956                Err(self.err(alloc::format!(
4957                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
4958                )))
4959            }
4960        } else {
4961            Err(self.err(alloc::format!(
4962                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
4963            )))
4964        }
4965    }
4966
4967    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
4968    /// SPG doesn't have a registry; pgvector / similar are
4969    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
4970    /// the syntax lets dual-target schemas keep the line.
4971    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
4972        // Optional `IF NOT EXISTS`.
4973        self.consume_if_not_exists();
4974        let name = self.expect_ident_like()?;
4975        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
4976        // CASCADE / FROM '<v>' clauses; we don't model them.
4977        loop {
4978            match self.peek() {
4979                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
4980                    self.advance();
4981                    continue;
4982                }
4983                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
4984                    self.advance();
4985                    let _ = self.expect_ident_like()?;
4986                    continue;
4987                }
4988                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
4989                    self.advance();
4990                    // String or ident literal.
4991                    let _ = self.advance();
4992                    continue;
4993                }
4994                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
4995                    self.advance();
4996                    let _ = self.advance();
4997                    continue;
4998                }
4999                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5000                    self.advance();
5001                    continue;
5002                }
5003                _ => break,
5004            }
5005        }
5006        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5007        // nosuch` reported success and `pg_extension` then did not list it,
5008        // which is the accept-and-do-nothing shape F31 exists to find.
5009        Ok(Statement::ValidateOnly {
5010            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5011            names: alloc::vec![name],
5012        })
5013    }
5014
5015    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5016    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5017    /// already been consumed by the caller. Grammar accepted:
5018    ///
5019    ///   name `(` arg-list `)`
5020    ///   `RETURNS` return-type
5021    ///   [ `LANGUAGE` ident ]
5022    ///   `AS` $$ body $$
5023    ///   [ `LANGUAGE` ident ]
5024    ///
5025    /// Either `LANGUAGE` position is allowed; PG accepts both.
5026    fn parse_create_function_after_keyword(
5027        &mut self,
5028        or_replace: bool,
5029    ) -> Result<Statement, ParseError> {
5030        let name = self.expect_ident_like()?;
5031        // Argument list. v7.12.4 commonly sees the empty `()`
5032        // (trigger functions); typed args parse and round-trip
5033        // but the executor only invokes nullary functions.
5034        if !matches!(self.peek(), Token::LParen) {
5035            return Err(self.err(alloc::format!(
5036                "expected '(' after function name {name:?}, got {:?}",
5037                self.peek()
5038            )));
5039        }
5040        self.advance();
5041        let args = self.parse_function_arg_list()?;
5042        // RETURNS clause.
5043        let tok = self.peek();
5044        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5045            return Err(self.err(alloc::format!(
5046                "expected RETURNS after function arg list, got {tok:?}"
5047            )));
5048        };
5049        if !s.eq_ignore_ascii_case("returns") {
5050            return Err(self.err(alloc::format!(
5051                "expected RETURNS after function arg list, got {s:?}"
5052            )));
5053        }
5054        self.advance();
5055        let returns = self.parse_function_return()?;
5056        // Optional LANGUAGE clause (PG also accepts after AS — we'll
5057        // re-check after the body too).
5058        let mut language: Option<String> = self.parse_optional_language()?;
5059        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5060        // either side of the body and in any order, interleaved with
5061        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5062        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5063        // PG's own pg_dump output did not restore.
5064        let mut attrs = FunctionAttrs::default();
5065        loop {
5066            let before = self.pos;
5067            self.parse_function_attrs_into(&mut attrs)?;
5068            if language.is_none() {
5069                language = self.parse_optional_language()?;
5070            }
5071            if self.pos == before {
5072                break;
5073            }
5074        }
5075        // `AS` followed by a $$-quoted body (lexer already
5076        // collapses both `$$…$$` and `$tag$…$tag$` to a single
5077        // Token::String). AS is a reserved keyword (Token::As).
5078        if !matches!(self.peek(), Token::As) {
5079            return Err(self.err(alloc::format!(
5080                "expected AS before function body, got {:?}",
5081                self.peek()
5082            )));
5083        }
5084        self.advance();
5085        let body_text = match self.peek() {
5086            Token::String(s) => {
5087                let body = s.clone();
5088                self.advance();
5089                body
5090            }
5091            other => {
5092                return Err(self.err(alloc::format!(
5093                    "expected $$-quoted function body after AS, got {other:?}"
5094                )));
5095            }
5096        };
5097        // Trailing clauses — PG's other accepted position for both the
5098        // LANGUAGE and the attributes.
5099        loop {
5100            let before = self.pos;
5101            self.parse_function_attrs_into(&mut attrs)?;
5102            if language.is_none() {
5103                language = self.parse_optional_language()?;
5104            }
5105            if self.pos == before {
5106                break;
5107            }
5108        }
5109        let language = language.unwrap_or_else(|| String::from("sql"));
5110        // PL/pgSQL bodies get structure-parsed. Other languages
5111        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5112        // recognise) round-trip as Raw text — the executor errors
5113        // when invoked with a clear unsupported message.
5114        let body = if language.eq_ignore_ascii_case("plpgsql") {
5115            match parse_plpgsql_body(&body_text) {
5116                Ok(block) => FunctionBody::PlPgSql(block),
5117                // Best-effort: if the body parser doesn't yet
5118                // support a construct used inside, fall back to
5119                // raw — keeps `CREATE FUNCTION` itself working
5120                // (catalogue accepts), executor errors on
5121                // invocation only.
5122                Err(_) => FunctionBody::Raw(body_text),
5123            }
5124        } else {
5125            FunctionBody::Raw(body_text)
5126        };
5127        Ok(Statement::CreateFunction(CreateFunctionStatement {
5128            name,
5129            or_replace,
5130            args,
5131            returns,
5132            language,
5133            body,
5134            attrs,
5135        }))
5136    }
5137
5138    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5139    /// attribute clauses into `attrs`, stopping at the first token that
5140    /// is not one. Measured against PG 18.4, which accepts them in any
5141    /// order and on either side of the body.
5142    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5143        loop {
5144            let word = match self.peek() {
5145                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5146                // NOT LEAKPROOF — NOT is a reserved keyword token.
5147                Token::Not
5148                    if matches!(
5149                        self.tokens.get(self.pos + 1),
5150                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5151                    ) =>
5152                {
5153                    self.advance();
5154                    self.advance();
5155                    attrs.leakproof = false;
5156                    continue;
5157                }
5158                _ => return Ok(()),
5159            };
5160            match word.as_str() {
5161                "immutable" => {
5162                    self.advance();
5163                    attrs.volatility = FunctionVolatility::Immutable;
5164                }
5165                "stable" => {
5166                    self.advance();
5167                    attrs.volatility = FunctionVolatility::Stable;
5168                }
5169                "volatile" => {
5170                    self.advance();
5171                    attrs.volatility = FunctionVolatility::Volatile;
5172                }
5173                "strict" => {
5174                    self.advance();
5175                    attrs.strict = true;
5176                }
5177                "leakproof" => {
5178                    self.advance();
5179                    attrs.leakproof = true;
5180                }
5181                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5182                // spelled-out forms of STRICT and its opposite.
5183                "returns" | "called" => {
5184                    let strict = word == "returns";
5185                    let mut probe = self.pos + 1;
5186                    if strict {
5187                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5188                        // is not ours.
5189                        match self.tokens.get(probe) {
5190                            Some(Token::Null) => probe += 1,
5191                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5192                            _ => return Ok(()),
5193                        }
5194                    }
5195                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5196                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5197                    if !ok {
5198                        return Ok(());
5199                    }
5200                    probe += 1;
5201                    match self.tokens.get(probe) {
5202                        Some(Token::Null) => probe += 1,
5203                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5204                        _ => return Ok(()),
5205                    }
5206                    match self.tokens.get(probe) {
5207                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5208                        _ => return Ok(()),
5209                    }
5210                    self.pos = probe;
5211                    attrs.strict = strict;
5212                }
5213                "security" | "external" => {
5214                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5215                    let mut probe = self.pos + 1;
5216                    if word == "external" {
5217                        match self.tokens.get(probe) {
5218                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5219                                probe += 1;
5220                            }
5221                            _ => return Ok(()),
5222                        }
5223                    }
5224                    let definer = match self.tokens.get(probe) {
5225                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5226                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5227                        _ => return Ok(()),
5228                    };
5229                    self.pos = probe + 1;
5230                    attrs.security_definer = definer;
5231                }
5232                "parallel" => {
5233                    let level = match self.tokens.get(self.pos + 1) {
5234                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5235                            FunctionParallel::Safe
5236                        }
5237                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5238                            FunctionParallel::Restricted
5239                        }
5240                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5241                            FunctionParallel::Unsafe
5242                        }
5243                        _ => return Ok(()),
5244                    };
5245                    self.pos += 2;
5246                    attrs.parallel = level;
5247                }
5248                "cost" | "rows" => {
5249                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5250                        return Ok(());
5251                    };
5252                    self.pos += 2;
5253                    if word == "cost" {
5254                        attrs.cost = Some(n);
5255                    } else {
5256                        attrs.rows = Some(n);
5257                    }
5258                }
5259                _ => return Ok(()),
5260            }
5261        }
5262    }
5263
5264    /// The numeric literal at `idx`, if there is one.
5265    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5266        match self.tokens.get(idx)? {
5267            Token::Integer(n) => Some(*n as f64),
5268            Token::Float(f) => Some(*f),
5269            Token::Numeric(t) => t.parse::<f64>().ok(),
5270            _ => None,
5271        }
5272    }
5273
5274    /// Closing `)`-terminated argument list. v7.12.4 commonly
5275    /// sees the empty `()`; typed args round-trip but the
5276    /// executor (yet) doesn't invoke them.
5277    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5278    /// it away, which is what PG does with one on a function parameter.
5279    fn skip_type_modifier(&mut self) {
5280        if !matches!(self.peek(), Token::LParen) {
5281            return;
5282        }
5283        // Only a numeric modifier — anything else is not one, and eating
5284        // it would swallow real grammar.
5285        let mut i = self.pos + 1;
5286        let mut seen_number = false;
5287        loop {
5288            match self.tokens.get(i) {
5289                Some(Token::Integer(_)) => seen_number = true,
5290                Some(Token::Comma) => {}
5291                Some(Token::RParen) => break,
5292                _ => return,
5293            }
5294            i += 1;
5295        }
5296        if !seen_number {
5297            return;
5298        }
5299        while self.pos <= i {
5300            self.advance();
5301        }
5302    }
5303
5304    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5305        let mut args: Vec<FunctionArg> = Vec::new();
5306        if matches!(self.peek(), Token::RParen) {
5307            self.advance();
5308            return Ok(args);
5309        }
5310        loop {
5311            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5312            // a reserved token; OUT / INOUT are bare idents.
5313            let mode = if matches!(self.peek(), Token::In) {
5314                self.advance();
5315                FunctionArgMode::In
5316            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5317            {
5318                self.advance();
5319                FunctionArgMode::Out
5320            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5321            {
5322                self.advance();
5323                FunctionArgMode::InOut
5324            } else {
5325                FunctionArgMode::In
5326            };
5327            // Optional name. The next token is either a name
5328            // (followed by a type ident) or the type itself.
5329            // Disambiguate by peeking ahead: if the token after
5330            // the next ident is also an ident, we treat the
5331            // first as the name.
5332            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5333            // the comma or paren, then decide. Reading at most two of
5334            // them could not spell `x double precision` at all, and
5335            // silently mis-read the bare `double precision` as a
5336            // parameter named "double" — which is what made the same
5337            // signature key two different ways.
5338            let (name, ty_token) = {
5339                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5340                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5341                    words.push(self.expect_ident_like()?);
5342                }
5343                // v7.39 (round 344) — a length / precision modifier on the
5344                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5345                // accepts it and DROPS it — `pg_get_function_arguments`
5346                // reports plain `character varying` / `numeric`, measured on
5347                // 18.4 — but SPG raised `syntax error at or near "("`,
5348                // because the modifier's parens were never consumed.
5349                self.skip_type_modifier();
5350                // r1049 — `f(v bigint[])`. The array suffix parsed in
5351                // the column position, the cast position and (r1038)
5352                // the RETURNS position, but not here: the fifth
5353                // member of the same family, reported by sentori as
5354                // presumably the same code. It is now.
5355                let array_suffix = self.consume_array_suffix();
5356                let whole = words.join(" ");
5357                let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5358                {
5359                    (Some(words[0].clone()), words[1..].join(" "))
5360                } else {
5361                    (None, whole)
5362                };
5363                ty_token.push_str(&array_suffix);
5364                (name, ty_token)
5365            };
5366            // Type — try to map to ColumnTypeName, else Raw.
5367            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5368                Some(t) => FunctionArgType::Typed(t),
5369                None => FunctionArgType::Raw(ty_token),
5370            };
5371            args.push(FunctionArg { mode, name, ty });
5372            match self.peek() {
5373                Token::Comma => {
5374                    self.advance();
5375                    continue;
5376                }
5377                Token::RParen => {
5378                    self.advance();
5379                    return Ok(args);
5380                }
5381                other => {
5382                    return Err(self.err(alloc::format!(
5383                        "expected , or ) in function arg list, got {other:?}"
5384                    )));
5385                }
5386            }
5387        }
5388    }
5389
5390    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5391        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5392        // function whose row shape is named inline.
5393        if matches!(self.peek(), Token::Table)
5394            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5395        {
5396            self.advance(); // TABLE
5397            self.advance(); // (
5398            let mut cols: Vec<String> = Vec::new();
5399            loop {
5400                let cname = self.expect_ident_like()?;
5401                let mut ty: Vec<String> = Vec::new();
5402                loop {
5403                    match self.peek() {
5404                        Token::Comma | Token::RParen | Token::Eof => break,
5405                        _ => {}
5406                    }
5407                    match self.advance() {
5408                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5409                        other => {
5410                            if let Some(w) = unreserved_keyword_text(&other) {
5411                                ty.push(w);
5412                            }
5413                        }
5414                    }
5415                }
5416                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5417                if matches!(self.peek(), Token::Comma) {
5418                    self.advance();
5419                } else {
5420                    break;
5421                }
5422            }
5423            if matches!(self.peek(), Token::RParen) {
5424                self.advance();
5425            }
5426            return Ok(FunctionReturn::Other(alloc::format!(
5427                "TABLE({})",
5428                cols.join(", ")
5429            )));
5430        }
5431        let ident = self.expect_ident_like()?;
5432        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5433        if ident.eq_ignore_ascii_case("setof") {
5434            let inner = self.expect_ident_like()?;
5435            let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5436            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5437        }
5438        if ident.eq_ignore_ascii_case("trigger") {
5439            return Ok(FunctionReturn::Trigger);
5440        }
5441        if ident.eq_ignore_ascii_case("void") {
5442            return Ok(FunctionReturn::Void);
5443        }
5444        // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5445        // RETURN position did not, so the `[` was a syntax error and the
5446        // whole migration stopped. sentori worked around it by returning
5447        // zero-padded text.
5448        let suffix = self.consume_array_suffix();
5449        if !suffix.is_empty() {
5450            return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5451        }
5452        match map_type_ident_to_column_type_name(&ident) {
5453            Some(t) => Ok(FunctionReturn::Type(t)),
5454            None => Ok(FunctionReturn::Other(ident)),
5455        }
5456    }
5457
5458    /// Consume any `[]` / `[N]` array markers after a type name and give
5459    /// back their text. Empty when there are none.
5460    fn consume_array_suffix(&mut self) -> String {
5461        let mut out = String::new();
5462        while matches!(self.peek(), Token::LBracket) {
5463            self.advance();
5464            // `[N]` is accepted and, as in PG, the length is not enforced.
5465            if let Token::Integer(n) = self.peek().clone() {
5466                self.advance();
5467                out.push_str(&alloc::format!("[{n}]"));
5468            } else {
5469                out.push_str("[]");
5470            }
5471            if matches!(self.peek(), Token::RBracket) {
5472                self.advance();
5473            }
5474        }
5475        out
5476    }
5477
5478    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5479        match self.peek() {
5480            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5481                self.advance();
5482                let lang = self.expect_ident_like()?;
5483                Ok(Some(lang.to_ascii_lowercase()))
5484            }
5485            _ => Ok(None),
5486        }
5487    }
5488
5489    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5490    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5491    /// (expr)]*`. The `DOMAIN` keyword has already been
5492    /// consumed. PG allows the trailing constraints in any
5493    /// order; we approximate with a small loop.
5494    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5495        let name = self.expect_ident_like()?;
5496        // Optional `AS`.
5497        if matches!(self.peek(), Token::As) {
5498            self.advance();
5499        }
5500        // v7.39 (round 259) — keep the raw type NAME when the base is not
5501        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5502        // parent domain.
5503        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5504            self.parse_type_with_implied_flags()?;
5505        let mut default: Option<Expr> = None;
5506        let mut not_null = false;
5507        let mut checks: Vec<Expr> = Vec::new();
5508        loop {
5509            match self.peek() {
5510                Token::Default => {
5511                    if default.is_some() {
5512                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5513                    }
5514                    self.advance();
5515                    default = Some(self.parse_expr(0)?);
5516                }
5517                Token::Not => {
5518                    self.advance();
5519                    if !matches!(self.peek(), Token::Null) {
5520                        return Err(self.err(alloc::format!(
5521                            "expected NULL after NOT in DOMAIN, got {:?}",
5522                            self.peek()
5523                        )));
5524                    }
5525                    self.advance();
5526                    not_null = true;
5527                }
5528                Token::Null => {
5529                    self.advance();
5530                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5531                    // is the default-nullable marker (PG accepts it),
5532                    // but AFTER a NOT NULL it is a conflict PG refuses
5533                    // (`conflicting NULL/NOT NULL constraints`,
5534                    // PG18-measured); the old arm no-opped both ways.
5535                    if not_null {
5536                        return Err(self.err(alloc::string::String::from(
5537                            "conflicting NULL/NOT NULL constraints",
5538                        )));
5539                    }
5540                }
5541                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5542                    self.advance();
5543                    if !matches!(self.peek(), Token::LParen) {
5544                        return Err(self.err(alloc::format!(
5545                            "expected '(' after CHECK in DOMAIN, got {:?}",
5546                            self.peek()
5547                        )));
5548                    }
5549                    self.advance();
5550                    let expr = self.parse_expr(0)?;
5551                    if !matches!(self.peek(), Token::RParen) {
5552                        return Err(self.err(alloc::format!(
5553                            "expected ')' after CHECK expr, got {:?}",
5554                            self.peek()
5555                        )));
5556                    }
5557                    self.advance();
5558                    checks.push(expr);
5559                }
5560                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5561                // prefix on the constraint; we drop the name and
5562                // recurse into the constraint parsing.
5563                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5564                    self.advance();
5565                    let _ = self.expect_ident_like()?;
5566                }
5567                _ => break,
5568            }
5569        }
5570        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5571            name,
5572            base_type,
5573            base_domain: base_user_ref,
5574            default,
5575            not_null,
5576            checks,
5577        }))
5578    }
5579
5580    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5581    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5582    /// consumed.
5583    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5584        let name = self.expect_ident_like()?;
5585        // Required `AS`.
5586        if !matches!(self.peek(), Token::As) {
5587            return Err(self.err(alloc::format!(
5588                "expected AS after CREATE TYPE {name:?}, got {:?}",
5589                self.peek()
5590            )));
5591        }
5592        self.advance();
5593        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5594        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5595        // on the next token: `(` = composite, ident `ENUM` = enum.
5596        if matches!(self.peek(), Token::LParen) {
5597            self.advance();
5598            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5599            let mut field_user_types: Vec<Option<String>> = Vec::new();
5600            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5601            // is legal PG (an attribute-less composite; measured — the old
5602            // e2e note claimed PG requires at least one attribute).
5603            if matches!(self.peek(), Token::RParen) {
5604                self.advance();
5605                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5606                    name,
5607                    kind: crate::ast::TypeKind::Composite {
5608                        fields,
5609                        field_user_types,
5610                    },
5611                }));
5612            }
5613            loop {
5614                let field_name = self.expect_ident_like()?;
5615                // v7.39 (round 264) — keep the raw type name when it is not
5616                // a builtin: that is how a NESTED composite field records
5617                // which composite it holds.
5618                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5619                    self.parse_type_with_implied_flags()?;
5620                fields.push((field_name, field_type));
5621                field_user_types.push(field_user_ref);
5622                if matches!(self.peek(), Token::Comma) {
5623                    self.advance();
5624                    continue;
5625                }
5626                if matches!(self.peek(), Token::RParen) {
5627                    self.advance();
5628                    break;
5629                }
5630                return Err(self.err(alloc::format!(
5631                    "expected , or ) in composite field list, got {:?}",
5632                    self.peek()
5633                )));
5634            }
5635            if fields.is_empty() {
5636                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5637            }
5638            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5639                name,
5640                kind: crate::ast::TypeKind::Composite {
5641                    fields,
5642                    field_user_types,
5643                },
5644            }));
5645        }
5646        // Required `ENUM` ident.
5647        let kind_ident = match self.peek().clone() {
5648            Token::Ident(s) | Token::QuotedIdent(s) => s,
5649            other => {
5650                return Err(self.err(alloc::format!(
5651                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5652                )));
5653            }
5654        };
5655        if !kind_ident.eq_ignore_ascii_case("enum") {
5656            return Err(self.err(alloc::format!(
5657                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5658            )));
5659        }
5660        self.advance();
5661        if !matches!(self.peek(), Token::LParen) {
5662            return Err(self.err(alloc::format!(
5663                "expected '(' after ENUM, got {:?}",
5664                self.peek()
5665            )));
5666        }
5667        self.advance();
5668        let mut labels: Vec<String> = Vec::new();
5669        loop {
5670            match self.peek().clone() {
5671                Token::String(s) => {
5672                    self.advance();
5673                    labels.push(s);
5674                }
5675                other => {
5676                    return Err(
5677                        self.err(alloc::format!("expected enum label string, got {other:?}"))
5678                    );
5679                }
5680            }
5681            if matches!(self.peek(), Token::Comma) {
5682                self.advance();
5683                continue;
5684            }
5685            if matches!(self.peek(), Token::RParen) {
5686                self.advance();
5687                break;
5688            }
5689            return Err(self.err(alloc::format!(
5690                "expected , or ) in ENUM label list, got {:?}",
5691                self.peek()
5692            )));
5693        }
5694        if labels.is_empty() {
5695            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5696        }
5697        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5698            name,
5699            kind: crate::ast::TypeKind::Enum { labels },
5700        }))
5701    }
5702
5703    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5704    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5705    /// The `CREATE MATERIALIZED VIEW` keywords have already been
5706    /// consumed.
5707    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5708        let if_not_exists = self.parse_if_not_exists();
5709        let name = self.expect_ident_like()?;
5710        let mut columns: Vec<String> = Vec::new();
5711        if matches!(self.peek(), Token::LParen) {
5712            self.advance();
5713            loop {
5714                let c = self.expect_ident_like()?;
5715                columns.push(c);
5716                if matches!(self.peek(), Token::Comma) {
5717                    self.advance();
5718                    continue;
5719                }
5720                if matches!(self.peek(), Token::RParen) {
5721                    self.advance();
5722                    break;
5723                }
5724                return Err(self.err(alloc::format!(
5725                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5726                    self.peek()
5727                )));
5728            }
5729        }
5730        if !matches!(self.peek(), Token::As) {
5731            return Err(self.err(alloc::format!(
5732                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5733                self.peek()
5734            )));
5735        }
5736        self.advance();
5737        // v7.39 (round 151) — a WITH-headed body is legal (read-only
5738        // CTEs only; the engine rejects data-modifying ones with PG's
5739        // message). A trailing `WITH [NO] DATA` can't START the body,
5740        // so WITH here heads the query.
5741        let body = if self.peek_is_with_kw() {
5742            self.advance();
5743            self.parse_nested_with_select()?
5744        } else {
5745            let body_stmt = self.parse_select_stmt()?;
5746            let Statement::Select(body) = body_stmt else {
5747                return Err(self.err(alloc::format!(
5748                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5749                )));
5750            };
5751            body
5752        };
5753        // Optional trailing `WITH [NO] DATA`.
5754        let with_data = self.parse_optional_with_data(true)?;
5755        Ok(Statement::CreateMaterializedView(
5756            crate::ast::CreateMaterializedViewStatement {
5757                temporary: false,
5758                name,
5759                if_not_exists,
5760                columns,
5761                body,
5762                with_data,
5763                as_plain_table: false,
5764            },
5765        ))
5766    }
5767
5768    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5769    /// `default_when_absent` is what to return if the tail is
5770    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5771    /// WITH DATA).
5772    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5773        let save = self.pos;
5774        // `WITH` is an Ident (not reserved in the lexer).
5775        let is_with = match self.peek() {
5776            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5777            _ => false,
5778        };
5779        if !is_with {
5780            return Ok(default_when_absent);
5781        }
5782        self.advance();
5783        // Optional `NO`.
5784        let mut with_data = true;
5785        let is_no = match self.peek() {
5786            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5787            _ => false,
5788        };
5789        if is_no {
5790            self.advance();
5791            with_data = false;
5792        }
5793        // Required `DATA` ident.
5794        let is_data = match self.peek() {
5795            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5796            _ => false,
5797        };
5798        if is_data {
5799            self.advance();
5800            Ok(with_data)
5801        } else {
5802            // Caller's WITH wasn't WITH-DATA — rewind so the outer
5803            // parser can interpret it.
5804            self.pos = save;
5805            Ok(default_when_absent)
5806        }
5807    }
5808
5809    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5810    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5811    /// All keyword prefixes have already been consumed; the flags
5812    /// say which were present.
5813    fn parse_create_view_after_keyword(
5814        &mut self,
5815        or_replace: bool,
5816        _materialized_unused: bool,
5817        temporary: bool,
5818    ) -> Result<Statement, ParseError> {
5819        let if_not_exists = self.parse_if_not_exists();
5820        let name = self.expect_ident_like()?;
5821        // Optional `(col, col, …)` rename list.
5822        let mut columns: Vec<String> = Vec::new();
5823        if matches!(self.peek(), Token::LParen) {
5824            self.advance();
5825            loop {
5826                let c = self.expect_ident_like()?;
5827                columns.push(c);
5828                if matches!(self.peek(), Token::Comma) {
5829                    self.advance();
5830                    continue;
5831                }
5832                if matches!(self.peek(), Token::RParen) {
5833                    self.advance();
5834                    break;
5835                }
5836                return Err(self.err(alloc::format!(
5837                    "expected , or ) in VIEW column list, got {:?}",
5838                    self.peek()
5839                )));
5840            }
5841        }
5842        // Required `AS`.
5843        if !matches!(self.peek(), Token::As) {
5844            return Err(self.err(alloc::format!(
5845                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5846                self.peek()
5847            )));
5848        }
5849        self.advance();
5850        // Body: a regular SELECT statement. v7.39 (round 151) — a
5851        // WITH-headed body is legal too (read-only CTEs only; the
5852        // engine rejects data-modifying ones with PG's message).
5853        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5854        // with the check-option clause, so WITH here heads the query.
5855        let body = if self.peek_is_with_kw() {
5856            self.advance();
5857            self.parse_nested_with_select()?
5858        } else {
5859            let body_stmt = self.parse_select_stmt()?;
5860            let Statement::Select(body) = body_stmt else {
5861                return Err(self.err(alloc::format!(
5862                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5863                )));
5864            };
5865            body
5866        };
5867        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5868        // The SELECT parser stops before a trailing WITH, so it lands here.
5869        let check_option = if matches!(self.peek(),
5870            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5871        {
5872            self.advance(); // WITH
5873            let opt = match self.peek() {
5874                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
5875                    self.advance();
5876                    crate::ast::ViewCheckOption::Local
5877                }
5878                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
5879                    self.advance();
5880                    crate::ast::ViewCheckOption::Cascaded
5881                }
5882                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
5883                _ => crate::ast::ViewCheckOption::Cascaded,
5884            };
5885            if !matches!(self.peek(),
5886                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
5887            {
5888                return Err(self.err(alloc::format!(
5889                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
5890                    self.peek()
5891                )));
5892            }
5893            self.advance(); // CHECK
5894            if !matches!(self.peek(),
5895                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
5896            {
5897                return Err(self.err(alloc::format!(
5898                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
5899                    self.peek()
5900                )));
5901            }
5902            self.advance(); // OPTION
5903            Some(opt)
5904        } else {
5905            None
5906        };
5907        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
5908            name,
5909            or_replace,
5910            if_not_exists,
5911            temporary,
5912            columns,
5913            body,
5914            check_option,
5915        }))
5916    }
5917
5918    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
5919    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
5920    /// consumed; `temporary` carries whether TEMPORARY was seen.
5921    fn parse_create_sequence_after_keyword(
5922        &mut self,
5923        temporary: bool,
5924    ) -> Result<Statement, ParseError> {
5925        let if_not_exists = self.parse_if_not_exists();
5926        let name = self.expect_ident_like()?;
5927        // Optional `AS data_type`.
5928        let data_type = if matches!(self.peek(), Token::As) {
5929            self.advance();
5930            Some(self.parse_sequence_data_type()?)
5931        } else {
5932            None
5933        };
5934        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
5935        Ok(Statement::CreateSequence(
5936            crate::ast::CreateSequenceStatement {
5937                name,
5938                if_not_exists,
5939                temporary,
5940                data_type,
5941                options,
5942            },
5943        ))
5944    }
5945
5946    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
5947    /// already been consumed; this is reached after `SEQUENCE`.
5948    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
5949    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5950        use crate::ast::AlterDomainAction as A;
5951        let name = self.expect_ident_like()?;
5952        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
5953        let kw = match self.peek() {
5954            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
5955            Token::Drop => alloc::string::String::from("drop"),
5956            Token::Default => alloc::string::String::from("default"),
5957            other => {
5958                return Err(self.err(alloc::format!(
5959                    "expected an ALTER DOMAIN action, got {other:?}"
5960                )));
5961            }
5962        };
5963        let action = match kw.as_str() {
5964            "add" => {
5965                self.advance();
5966                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
5967                {
5968                    self.advance();
5969                    Some(self.expect_ident_like()?)
5970                } else {
5971                    None
5972                };
5973                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
5974                    return Err(self.err(alloc::format!(
5975                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
5976                        self.peek()
5977                    )));
5978                }
5979                self.advance();
5980                if !matches!(self.peek(), Token::LParen) {
5981                    return Err(self.err("expected '(' after CHECK".into()));
5982                }
5983                self.advance();
5984                let check = self.parse_expr(0)?;
5985                if !matches!(self.peek(), Token::RParen) {
5986                    return Err(self.err("expected ')' after CHECK expression".into()));
5987                }
5988                self.advance();
5989                A::AddConstraint { name: cname, check }
5990            }
5991            "drop" => {
5992                self.advance();
5993                match self.peek() {
5994                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
5995                        self.advance();
5996                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
5997                        {
5998                            self.advance();
5999                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6000                            {
6001                                return Err(self.err("expected EXISTS after IF".into()));
6002                            }
6003                            self.advance();
6004                            true
6005                        } else {
6006                            false
6007                        };
6008                        let cn = self.expect_ident_like()?;
6009                        A::DropConstraint {
6010                            name: cn,
6011                            if_exists,
6012                        }
6013                    }
6014                    Token::Default => {
6015                        self.advance();
6016                        A::DropDefault
6017                    }
6018                    Token::Not => {
6019                        self.advance();
6020                        if !matches!(self.peek(), Token::Null) {
6021                            return Err(self.err("expected NULL after NOT".into()));
6022                        }
6023                        self.advance();
6024                        A::DropNotNull
6025                    }
6026                    other => {
6027                        return Err(self.err(alloc::format!(
6028                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6029                        )));
6030                    }
6031                }
6032            }
6033            "set" => {
6034                self.advance();
6035                match self.peek() {
6036                    Token::Default => {
6037                        self.advance();
6038                        A::SetDefault(self.parse_expr(0)?)
6039                    }
6040                    Token::Not => {
6041                        self.advance();
6042                        if !matches!(self.peek(), Token::Null) {
6043                            return Err(self.err("expected NULL after NOT".into()));
6044                        }
6045                        self.advance();
6046                        A::SetNotNull
6047                    }
6048                    other => {
6049                        return Err(self.err(alloc::format!(
6050                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6051                        )));
6052                    }
6053                }
6054            }
6055            "rename" => {
6056                self.advance();
6057                if !matches!(self.peek(), Token::To) {
6058                    return Err(self.err("expected TO after RENAME".into()));
6059                }
6060                self.advance();
6061                A::RenameTo(self.expect_ident_like()?)
6062            }
6063            other => {
6064                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6065            }
6066        };
6067        Ok(Statement::AlterDomain { name, action })
6068    }
6069
6070    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6071        let if_exists = self.parse_if_exists();
6072        let name = self.expect_ident_like()?;
6073        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6074        // the option list (PG allows only one or the other).
6075        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6076            self.advance();
6077            if matches!(self.peek(), Token::To) {
6078                self.advance();
6079            } else {
6080                self.expect_keyword_ident("to")?;
6081            }
6082            let new = self.expect_ident_like()?;
6083            return Ok(Statement::AlterSequence(
6084                crate::ast::AlterSequenceStatement {
6085                    name,
6086                    if_exists,
6087                    options: crate::ast::SequenceOptions::default(),
6088                    rename_to: Some(new),
6089                },
6090            ));
6091        }
6092        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6093        Ok(Statement::AlterSequence(
6094            crate::ast::AlterSequenceStatement {
6095                name,
6096                if_exists,
6097                options,
6098                rename_to: None,
6099            },
6100        ))
6101    }
6102
6103    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6104        let kw = self.expect_ident_like()?;
6105        match kw.to_ascii_lowercase().as_str() {
6106            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6107            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6108            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6109            other => Err(self.err(alloc::format!(
6110                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6111            ))),
6112        }
6113    }
6114
6115    fn parse_sequence_options(
6116        &mut self,
6117        allow_restart: bool,
6118    ) -> Result<crate::ast::SequenceOptions, ParseError> {
6119        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6120        let mut opts = SequenceOptions::default();
6121        #[allow(clippy::while_let_loop)]
6122        loop {
6123            // Match an ident; stop at any non-ident token (sentinel,
6124            // semicolon, end of statement).
6125            let kw_lc = match self.peek() {
6126                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6127                _ => break,
6128            };
6129            match kw_lc.as_str() {
6130                "increment" => {
6131                    self.advance();
6132                    // Optional BY.
6133                    if self.peek_is_by() {
6134                        self.advance();
6135                    }
6136                    opts.increment = Some(self.expect_signed_int()?);
6137                }
6138                "minvalue" => {
6139                    self.advance();
6140                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6141                }
6142                "maxvalue" => {
6143                    self.advance();
6144                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6145                }
6146                "no" => {
6147                    self.advance();
6148                    let what = self.expect_ident_like()?;
6149                    match what.to_ascii_lowercase().as_str() {
6150                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6151                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6152                        "cycle" => opts.cycle = Some(false),
6153                        other => {
6154                            return Err(self.err(alloc::format!(
6155                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6156                            )));
6157                        }
6158                    }
6159                }
6160                "start" => {
6161                    self.advance();
6162                    // Optional WITH.
6163                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6164                        if s.eq_ignore_ascii_case("with"))
6165                    {
6166                        self.advance();
6167                    }
6168                    opts.start = Some(self.expect_signed_int()?);
6169                }
6170                "restart" if allow_restart => {
6171                    self.advance();
6172                    // Optional WITH n; bare RESTART means restart at START.
6173                    let mut with_val: Option<i64> = None;
6174                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6175                        if s.eq_ignore_ascii_case("with"))
6176                    {
6177                        self.advance();
6178                        with_val = Some(self.expect_signed_int()?);
6179                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6180                        with_val = Some(self.expect_signed_int()?);
6181                    }
6182                    opts.restart = Some(with_val);
6183                }
6184                "cache" => {
6185                    self.advance();
6186                    opts.cache = Some(self.expect_signed_int()?);
6187                }
6188                "cycle" => {
6189                    self.advance();
6190                    opts.cycle = Some(true);
6191                }
6192                "owned" => {
6193                    self.advance();
6194                    match self.peek() {
6195                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6196                            self.advance();
6197                        }
6198                        other => {
6199                            return Err(
6200                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6201                            );
6202                        }
6203                    }
6204                    // OWNED BY {NONE | tab.col}. Read just one ident
6205                    // (NOT expect_ident_like which would auto-strip
6206                    // a schema prefix and consume the `.col` we need).
6207                    let first = match self.advance() {
6208                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6209                        other => {
6210                            return Err(self.err(alloc::format!(
6211                                "expected identifier or NONE after OWNED BY, got {other:?}"
6212                            )));
6213                        }
6214                    };
6215                    if first.eq_ignore_ascii_case("none") {
6216                        opts.owned_by = Some(SequenceOwnedBy::None);
6217                    } else if matches!(self.peek(), Token::Dot) {
6218                        self.advance();
6219                        let second = match self.advance() {
6220                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6221                            other => {
6222                                return Err(self.err(alloc::format!(
6223                                    "expected column name after OWNED BY {first}., got {other:?}"
6224                                )));
6225                            }
6226                        };
6227                        // v7.17 dump-compat fix — pg_dump emits
6228                        // OWNED BY clauses as
6229                        // `schema.table.column` (three segments).
6230                        // If a third `.<ident>` follows, treat the
6231                        // first ident as schema (drop it; SPG is
6232                        // single-schema) and the middle / last
6233                        // pair as table.column. Otherwise it's
6234                        // the two-segment form table.column.
6235                        if matches!(self.peek(), Token::Dot) {
6236                            self.advance();
6237                            let third = match self.advance() {
6238                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6239                                other => {
6240                                    return Err(self.err(alloc::format!(
6241                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6242                                    )));
6243                                }
6244                            };
6245                            let _ = first; // schema prefix discarded
6246                            opts.owned_by = Some(SequenceOwnedBy::Column {
6247                                table: second,
6248                                column: third,
6249                            });
6250                        } else {
6251                            opts.owned_by = Some(SequenceOwnedBy::Column {
6252                                table: first,
6253                                column: second,
6254                            });
6255                        }
6256                    } else {
6257                        return Err(self.err(alloc::format!(
6258                            "expected table.column or NONE after OWNED BY, got {first:?}"
6259                        )));
6260                    }
6261                }
6262                _ => break,
6263            }
6264        }
6265        Ok(opts)
6266    }
6267
6268    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6269        let neg = if matches!(self.peek(), Token::Minus) {
6270            self.advance();
6271            true
6272        } else {
6273            false
6274        };
6275        match self.peek() {
6276            Token::Integer(n) => {
6277                let v = *n;
6278                self.advance();
6279                Ok(if neg { -v } else { v })
6280            }
6281            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6282        }
6283    }
6284
6285    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6286    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6287    /// clause is fully accepted and discarded — SPG always runs
6288    /// constraint checks immediately (single-writer model). The
6289    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6290    /// in either order (per the SQL spec they're independent),
6291    /// though pg_dump always emits them in the canonical
6292    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6293    /// Stops at the first token that isn't part of the clause.
6294    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6295        self.consume_deferrable_clauses_timed().map(|_| ())
6296    }
6297
6298    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6299    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6300    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6301    /// NOT DEFERRABLE and a circular-FK migration could not load.
6302    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6303        let mut deferrable = false;
6304        let mut initially_deferred = false;
6305        loop {
6306            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6307            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6308                self.advance();
6309                deferrable = true;
6310                if self.consume_optional_initially_clause()? {
6311                    initially_deferred = true;
6312                }
6313                continue;
6314            }
6315            // `NOT DEFERRABLE` — already worked pre-3.1.
6316            if matches!(self.peek(), Token::Not) {
6317                let look = self.tokens.get(self.pos + 1);
6318                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6319                    self.advance(); // NOT
6320                    self.advance(); // DEFERRABLE
6321                    deferrable = false;
6322                    initially_deferred = false;
6323                    let _ = self.consume_optional_initially_clause()?;
6324                    continue;
6325                }
6326                break;
6327            }
6328            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6329            // accepts this without a leading [NOT] DEFERRABLE
6330            // (the timing keyword alone). pg_dump occasionally
6331            // emits it on FK constraints that inherit timing.
6332            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6333                if self.consume_optional_initially_clause()? {
6334                    initially_deferred = true;
6335                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6336                    deferrable = true;
6337                }
6338                continue;
6339            }
6340            break;
6341        }
6342        Ok((deferrable, initially_deferred))
6343    }
6344
6345    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6346    /// next token is `INITIALLY`, consume it plus the required
6347    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6348    /// Returns true when the timing seen was `DEFERRED`.
6349    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6350        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6351            return Ok(false);
6352        }
6353        self.advance(); // INITIALLY
6354        match self.advance() {
6355            Token::Ident(s)
6356                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6357            {
6358                Ok(s.eq_ignore_ascii_case("deferred"))
6359            }
6360            other => Err(self.err(alloc::format!(
6361                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6362            ))),
6363        }
6364    }
6365
6366    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6367    /// in its entirety so the parser returns Empty without
6368    /// touching the runtime. The CREATE+PROCEDURE keywords are
6369    /// already consumed; this swallows everything from the
6370    /// procedure name through the matching `END`, including
6371    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6372    /// (DELIMITER `//` makes the script splitter forward the
6373    /// whole block as one statement), `@var` session-variable
6374    /// references, and the trailing terminator.
6375    ///
6376    /// Tracks nesting depth so:
6377    ///   BEGIN
6378    ///     IF cond THEN
6379    ///       BEGIN ... END;
6380    ///     END IF;
6381    ///   END
6382    /// terminates at the outer END.
6383    fn consume_mysql_routine_body(&mut self) {
6384        // Outer skeleton: name, (...), optional clauses, BEGIN
6385        // <body> END [;]. Scan for the first BEGIN — anything
6386        // before it is signature decoration we don't care about.
6387        // Once inside BEGIN, count up on BEGIN, down on END.
6388        let mut depth: i32 = 0;
6389        let mut started = false;
6390        loop {
6391            match self.peek().clone() {
6392                Token::Begin => {
6393                    self.advance();
6394                    depth += 1;
6395                    started = true;
6396                }
6397                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6398                    self.advance();
6399                    if started {
6400                        depth -= 1;
6401                        if depth <= 0 {
6402                            // Optional trailing ident (`END IF`,
6403                            // `END LOOP`, `END WHILE`, `END CASE`,
6404                            // `END label_name`) — eat the next
6405                            // ident if present so we don't
6406                            // mistake `END IF;` for the outer
6407                            // close.
6408                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6409                                // If the next token is one of the
6410                                // PL/SQL block-closer keywords,
6411                                // the END belongs to an inner
6412                                // block; bump depth back up.
6413                                let is_inner_close = matches!(
6414                                    self.peek(),
6415                                    Token::Ident(s) | Token::QuotedIdent(s)
6416                                        if matches!(
6417                                            s.to_ascii_lowercase().as_str(),
6418                                            "if" | "loop" | "while" | "case" | "repeat"
6419                                        )
6420                                );
6421                                if is_inner_close {
6422                                    self.advance();
6423                                    depth += 1;
6424                                    continue;
6425                                }
6426                            }
6427                            // Eat optional trailing `;`.
6428                            if matches!(self.peek(), Token::Semicolon) {
6429                                self.advance();
6430                            }
6431                            return;
6432                        }
6433                    }
6434                }
6435                Token::Eof => return,
6436                _ => {
6437                    self.advance();
6438                }
6439            }
6440        }
6441    }
6442
6443    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6444    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6445    ///
6446    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6447    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6448    ///   ident, or `ident @ ident-or-quoted-string` host form)
6449    /// * `SQL SECURITY {DEFINER|INVOKER}`
6450    ///
6451    /// Each clause may appear at most once but in any order.
6452    /// The hints are pure planner / permission metadata that
6453    /// SPG's view-rewrite engine handles uniformly; we accept
6454    /// and discard. Returns `Ok(())` once a non-clause token is
6455    /// peeked (the caller then checks for the `VIEW` keyword).
6456    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6457        loop {
6458            match self.peek().clone() {
6459                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6460                    self.advance(); // ALGORITHM
6461                    // Optional `=`. MySQL spec requires it but be
6462                    // generous.
6463                    if matches!(self.peek(), Token::Eq) {
6464                        self.advance();
6465                    }
6466                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6467                    // bare ident; unknown values still parse so
6468                    // future MySQL versions don't break.
6469                    if matches!(
6470                        self.peek(),
6471                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6472                    ) {
6473                        self.advance();
6474                    }
6475                }
6476                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6477                    self.advance(); // DEFINER
6478                    if matches!(self.peek(), Token::Eq) {
6479                        self.advance();
6480                    }
6481                    // User: quoted string, ident, OR ident @ host
6482                    // (host may itself be quoted or bare).
6483                    match self.peek().clone() {
6484                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6485                            self.advance();
6486                            // Optional `@host`.
6487                            if matches!(self.peek(), Token::At) {
6488                                self.advance();
6489                                if matches!(
6490                                    self.peek(),
6491                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6492                                ) {
6493                                    self.advance();
6494                                }
6495                            }
6496                        }
6497                        _ => {}
6498                    }
6499                }
6500                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6501                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6502                    // when followed by SECURITY — the dispatcher must
6503                    // not consume a bare `SQL` token (it's not a
6504                    // legal CREATE prefix on its own).
6505                    let save = self.pos;
6506                    self.advance(); // SQL
6507                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6508                        if s2.eq_ignore_ascii_case("security"))
6509                    {
6510                        self.advance(); // SECURITY
6511                        // DEFINER / INVOKER trailing ident.
6512                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6513                            self.advance();
6514                        }
6515                    } else {
6516                        // Not a SQL SECURITY clause — roll back and
6517                        // bail; the caller will error out cleanly.
6518                        self.pos = save;
6519                        return Ok(());
6520                    }
6521                }
6522                _ => return Ok(()),
6523            }
6524        }
6525    }
6526
6527    fn parse_if_not_exists(&mut self) -> bool {
6528        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6529        {
6530            let save = self.pos;
6531            self.advance();
6532            if matches!(self.peek(), Token::Not) {
6533                self.advance();
6534                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6535                {
6536                    self.advance();
6537                    return true;
6538                }
6539            }
6540            self.pos = save;
6541        }
6542        false
6543    }
6544
6545    fn parse_if_exists(&mut self) -> bool {
6546        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6547        {
6548            let save = self.pos;
6549            self.advance();
6550            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6551            {
6552                self.advance();
6553                return true;
6554            }
6555            self.pos = save;
6556        }
6557        false
6558    }
6559
6560    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6561    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6562    /// been consumed.
6563    fn parse_create_trigger_after_keyword(
6564        &mut self,
6565        or_replace: bool,
6566    ) -> Result<Statement, ParseError> {
6567        let name = self.expect_ident_like()?;
6568        let timing = {
6569            let ident = self.expect_ident_like()?;
6570            if ident.eq_ignore_ascii_case("before") {
6571                TriggerTiming::Before
6572            } else if ident.eq_ignore_ascii_case("after") {
6573                TriggerTiming::After
6574            } else if ident.eq_ignore_ascii_case("instead") {
6575                let next = self.expect_ident_like()?;
6576                if !next.eq_ignore_ascii_case("of") {
6577                    return Err(self.err(alloc::format!(
6578                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6579                    )));
6580                }
6581                TriggerTiming::InsteadOf
6582            } else {
6583                return Err(self.err(alloc::format!(
6584                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6585                )));
6586            }
6587        };
6588        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6589        // OR is a reserved keyword token (Token::Or), not an Ident.
6590        // v7.13.0 — after an UPDATE event we may optionally see
6591        // `OF col, col, …` (mailrs round-5 G7). Columns are
6592        // captured into `update_columns` once across the whole
6593        // events list; multiple `UPDATE OF` clauses are rejected.
6594        let mut events: Vec<TriggerEvent> = Vec::new();
6595        let mut update_columns: Vec<String> = Vec::new();
6596        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6597        events.push(first_ev);
6598        if !first_cols.is_empty() {
6599            update_columns = first_cols;
6600        }
6601        while matches!(self.peek(), Token::Or) {
6602            self.advance();
6603            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6604            events.push(ev);
6605            if !cols.is_empty() {
6606                if !update_columns.is_empty() {
6607                    return Err(
6608                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6609                    );
6610                }
6611                update_columns = cols;
6612            }
6613        }
6614        // ON <table>
6615        let tok = self.peek();
6616        let Token::On = tok else {
6617            return Err(self.err(alloc::format!(
6618                "expected ON after trigger events, got {tok:?}"
6619            )));
6620        };
6621        self.advance();
6622        let table = self.expect_ident_like()?;
6623        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6624        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6625        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6626        // the trigger as a plain AFTER trigger (correct for every non-deferred
6627        // use; deferral timing is not yet honoured).
6628        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6629            if s.eq_ignore_ascii_case("from"))
6630        {
6631            self.advance();
6632            let _reftable = self.expect_ident_like()?;
6633        }
6634        self.consume_optional_deferrable_clauses()?;
6635        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6636        // keyword (Token::For); EACH / ROW / STATEMENT are bare
6637        // idents.
6638        if !matches!(self.peek(), Token::For) {
6639            return Err(self.err(alloc::format!(
6640                "expected FOR EACH ROW / STATEMENT, got {:?}",
6641                self.peek()
6642            )));
6643        }
6644        self.advance();
6645        let for_each = {
6646            let e = self.expect_ident_like()?;
6647            if !e.eq_ignore_ascii_case("each") {
6648                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6649            }
6650            let unit = self.expect_ident_like()?;
6651            if unit.eq_ignore_ascii_case("row") {
6652                TriggerForEach::Row
6653            } else if unit.eq_ignore_ascii_case("statement") {
6654                TriggerForEach::Statement
6655            } else {
6656                return Err(self.err(alloc::format!(
6657                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6658                )));
6659            }
6660        };
6661        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6662        let when_condition = if matches!(self.peek(),
6663            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6664        {
6665            self.advance();
6666            Some(self.parse_paren_expr("WHEN")?)
6667        } else {
6668            None
6669        };
6670        // EXECUTE FUNCTION/PROCEDURE name(...)
6671        let exec = self.expect_ident_like()?;
6672        if !exec.eq_ignore_ascii_case("execute") {
6673            return Err(self.err(alloc::format!(
6674                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6675            )));
6676        }
6677        let fn_or_proc = self.expect_ident_like()?;
6678        if !(fn_or_proc.eq_ignore_ascii_case("function")
6679            || fn_or_proc.eq_ignore_ascii_case("procedure"))
6680        {
6681            return Err(self.err(alloc::format!(
6682                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6683            )));
6684        }
6685        let function = self.expect_ident_like()?;
6686        // Optional empty arg list `()`.
6687        if matches!(self.peek(), Token::LParen) {
6688            self.advance();
6689            if !matches!(self.peek(), Token::RParen) {
6690                return Err(self.err(alloc::format!(
6691                    "v7.12.4 trigger function calls take no args; got {:?}",
6692                    self.peek()
6693                )));
6694            }
6695            self.advance();
6696        }
6697        Ok(Statement::CreateTrigger(CreateTriggerStatement {
6698            name,
6699            or_replace,
6700            timing,
6701            events,
6702            table,
6703            for_each,
6704            function,
6705            update_columns,
6706            when_condition,
6707        }))
6708    }
6709
6710    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6711    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6712    fn parse_create_rule_after_keyword(
6713        &mut self,
6714        or_replace: bool,
6715    ) -> Result<Statement, ParseError> {
6716        let name = self.expect_ident_like()?;
6717        if !matches!(self.peek(), Token::As) {
6718            return Err(self.err(alloc::format!(
6719                "expected AS in CREATE RULE, got {:?}",
6720                self.peek()
6721            )));
6722        }
6723        self.advance();
6724        if !matches!(self.peek(), Token::On) {
6725            return Err(self.err(alloc::format!(
6726                "expected ON in CREATE RULE, got {:?}",
6727                self.peek()
6728            )));
6729        }
6730        self.advance();
6731        let event = self.parse_rule_event()?;
6732        if !matches!(self.peek(), Token::To)
6733            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6734        {
6735            return Err(self.err(alloc::format!(
6736                "expected TO after rule event, got {:?}",
6737                self.peek()
6738            )));
6739        }
6740        self.advance();
6741        let table = self.expect_ident_like()?;
6742        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6743        let when_condition = if matches!(self.peek(), Token::Where) {
6744            self.advance();
6745            Some(self.parse_expr(0)?)
6746        } else {
6747            None
6748        };
6749        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6750        {
6751            return Err(self.err(alloc::format!(
6752                "expected DO in CREATE RULE, got {:?}",
6753                self.peek()
6754            )));
6755        }
6756        self.advance();
6757        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6758        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6759        {
6760            self.advance();
6761            true
6762        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6763            self.advance();
6764            false
6765        } else {
6766            false
6767        };
6768        // `NOTHING` | `( cmd; … )` | `cmd`.
6769        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6770        {
6771            self.advance();
6772            Vec::new()
6773        } else if matches!(self.peek(), Token::LParen) {
6774            self.advance();
6775            let mut cmds = Vec::new();
6776            loop {
6777                cmds.push(self.parse_one_statement()?);
6778                if matches!(self.peek(), Token::Semicolon) {
6779                    self.advance();
6780                    if matches!(self.peek(), Token::RParen) {
6781                        break;
6782                    }
6783                    continue;
6784                }
6785                break;
6786            }
6787            if !matches!(self.peek(), Token::RParen) {
6788                return Err(self.err(alloc::format!(
6789                    "expected ) closing the CREATE RULE command list, got {:?}",
6790                    self.peek()
6791                )));
6792            }
6793            self.advance();
6794            cmds
6795        } else {
6796            alloc::vec![self.parse_one_statement()?]
6797        };
6798        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6799            name,
6800            or_replace,
6801            event,
6802            table,
6803            instead,
6804            when_condition,
6805            commands,
6806        }))
6807    }
6808
6809    /// v7.39 (round 139) — a rule event keyword → uppercase string.
6810    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6811        if matches!(self.peek(), Token::Insert) {
6812            self.advance();
6813            return Ok(alloc::string::String::from("INSERT"));
6814        }
6815        if matches!(self.peek(), Token::Select) {
6816            self.advance();
6817            return Ok(alloc::string::String::from("SELECT"));
6818        }
6819        match self.peek() {
6820            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6821                self.advance();
6822                Ok(alloc::string::String::from("UPDATE"))
6823            }
6824            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6825                self.advance();
6826                Ok(alloc::string::String::from("DELETE"))
6827            }
6828            other => Err(self.err(alloc::format!(
6829                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6830            ))),
6831        }
6832    }
6833
6834    /// v7.13.0 — parse one trigger event, then optionally consume
6835    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6836    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6837    fn parse_trigger_event_with_optional_of(
6838        &mut self,
6839    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6840        let ev = self.parse_trigger_event()?;
6841        if !matches!(ev, TriggerEvent::Update) {
6842            return Ok((ev, Vec::new()));
6843        }
6844        // `OF` is a bare ident.
6845        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6846            return Ok((ev, Vec::new()));
6847        }
6848        self.advance(); // OF
6849        let mut cols: Vec<String> = Vec::new();
6850        loop {
6851            cols.push(self.expect_ident_like()?);
6852            if matches!(self.peek(), Token::Comma) {
6853                self.advance();
6854                continue;
6855            }
6856            break;
6857        }
6858        if cols.is_empty() {
6859            return Err(
6860                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6861            );
6862        }
6863        Ok((ev, cols))
6864    }
6865
6866    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6867    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6868    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6869    /// inside the body.
6870    /// Called by [`parse_plpgsql_body`] after the body's tokens
6871    /// have been lexed into this temporary parser.
6872    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
6873        // v7.12.6 — optional DECLARE prelude.
6874        let declarations = if matches!(
6875            self.peek(),
6876            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
6877        ) {
6878            self.advance();
6879            self.parse_plpgsql_declare_block()?
6880        } else {
6881            Vec::new()
6882        };
6883        // BEGIN keyword (PL/pgSQL — distinct from the SQL
6884        // `BEGIN` transaction-start, but we can reuse the
6885        // reserved Token::Begin since the body is a separate
6886        // lex/parse context).
6887        if !matches!(self.peek(), Token::Begin) {
6888            return Err(self.err(alloc::format!(
6889                "expected BEGIN at start of plpgsql block, got {:?}",
6890                self.peek()
6891            )));
6892        }
6893        self.advance();
6894        let statements = self.parse_plpgsql_stmt_list_until_end()?;
6895        // v7.37.20 (20.10) — optional EXCEPTION clause between the
6896        // body's last statement and the trailing END. When present
6897        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
6898        // arms terminated by END.
6899        let exception_handlers = if matches!(
6900            self.peek(),
6901            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
6902        ) {
6903            self.advance();
6904            self.parse_plpgsql_exception_handlers()?
6905        } else {
6906            Vec::new()
6907        };
6908        Ok(PlPgSqlBlock {
6909            declarations,
6910            statements,
6911            exception_handlers,
6912        })
6913    }
6914
6915    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
6916    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
6917    fn parse_plpgsql_exception_handlers(
6918        &mut self,
6919    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
6920        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
6921        loop {
6922            // Stop at END — the block-level trailing END LOOP / END;
6923            // is handled by the caller.
6924            if matches!(
6925                self.peek(),
6926                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
6927            ) {
6928                return Ok(out);
6929            }
6930            // WHEN <cond> [OR <cond>]* THEN <body>
6931            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6932            {
6933                return Err(self.err(alloc::format!(
6934                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
6935                    self.peek()
6936                )));
6937            }
6938            self.advance();
6939            let mut conditions: Vec<String> = Vec::new();
6940            conditions.push(self.expect_ident_like()?);
6941            while matches!(self.peek(), Token::Or) {
6942                self.advance();
6943                conditions.push(self.expect_ident_like()?);
6944            }
6945            let then_kw = self.expect_ident_like()?;
6946            if !then_kw.eq_ignore_ascii_case("then") {
6947                return Err(self.err(alloc::format!(
6948                    "expected THEN after WHEN condition list, got {then_kw:?}"
6949                )));
6950            }
6951            let body = self.parse_plpgsql_stmt_list_until_end()?;
6952            out.push(crate::ast::ExceptionHandler { conditions, body });
6953        }
6954    }
6955
6956    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
6957    /// prelude. Caller has already consumed `DECLARE`. We stop
6958    /// reading entries when we hit `BEGIN`.
6959    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
6960        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
6961        loop {
6962            if matches!(self.peek(), Token::Begin) {
6963                return Ok(out);
6964            }
6965            let name = self.expect_ident_like()?;
6966            // v7.37.20 (20.7) — type inference: if the next token is
6967            // `:=` or `=` (no explicit type), infer from the default
6968            // expression. Otherwise the ident that follows is the
6969            // declared type.
6970            //
6971            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
6972            // (PG-standard). SPG parse-accepts and treats identically
6973            // to inference — the eventual runtime value determines
6974            // the local's type, which is faithful to how SPG handles
6975            // untyped locals today (see 20.7). Full compile-time
6976            // catalog lookup queues with v7.40 PL/pgSQL epic.
6977            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
6978                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
6979                // downstream declaration walker to type the local by
6980                // the runtime type of the default expression.
6981                FunctionArgType::Raw("_infer_".into())
6982            } else {
6983                let ty_token = self.expect_ident_like()?;
6984                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
6985                // consume optional `.<ident>` qualifier + `%<KW>`
6986                // suffix. Both qualifier and suffix map to _infer_.
6987                if matches!(self.peek(), Token::Dot) {
6988                    self.advance();
6989                    let _ = self.expect_ident_like()?;
6990                }
6991                if matches!(self.peek(), Token::Percent) {
6992                    self.advance();
6993                    // Consume the trailing TYPE / ROWTYPE ident.
6994                    let _ = self.expect_ident_like()?;
6995                    FunctionArgType::Raw("_infer_".into())
6996                } else {
6997                    match map_type_ident_to_column_type_name(&ty_token) {
6998                        Some(t) => FunctionArgType::Typed(t),
6999                        None => FunctionArgType::Raw(ty_token),
7000                    }
7001                }
7002            };
7003            let default = match self.peek() {
7004                Token::ColonEq => {
7005                    self.advance();
7006                    Some(self.parse_expr(0)?)
7007                }
7008                Token::Eq => {
7009                    // PL/pgSQL also accepts `=` for the
7010                    // DECLARE default (PG treats them the same
7011                    // in this position).
7012                    self.advance();
7013                    Some(self.parse_expr(0)?)
7014                }
7015                _ => None,
7016            };
7017            // Mandatory `;` between declarations.
7018            if !matches!(self.peek(), Token::Semicolon) {
7019                return Err(self.err(alloc::format!(
7020                    "expected ; after DECLARE entry for {name:?}, got {:?}",
7021                    self.peek()
7022                )));
7023            }
7024            self.advance();
7025            out.push(PlPgSqlDeclare { name, ty, default });
7026        }
7027    }
7028
7029    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7030    /// the terminating `END;` (or `END IF;` etc — handled by the
7031    /// per-construct sub-parsers). Used by both the outer block
7032    /// and the IF/ELSE branch bodies.
7033    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7034        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7035        loop {
7036            // Allow trailing semicolons + END.
7037            while matches!(self.peek(), Token::Semicolon) {
7038                self.advance();
7039            }
7040            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7041            if matches!(
7042                self.peek(),
7043                Token::Ident(s) | Token::QuotedIdent(s)
7044                    if s.eq_ignore_ascii_case("end")
7045                        || s.eq_ignore_ascii_case("else")
7046                        || s.eq_ignore_ascii_case("elsif")
7047                        || s.eq_ignore_ascii_case("elseif")
7048                        || s.eq_ignore_ascii_case("exception")
7049                        || s.eq_ignore_ascii_case("when")
7050            ) {
7051                return Ok(statements);
7052            }
7053            // Otherwise: one statement, then expect `;` or
7054            // a block-terminator keyword.
7055            let stmt = self.parse_plpgsql_stmt()?;
7056            statements.push(stmt);
7057            match self.peek() {
7058                Token::Semicolon => {
7059                    self.advance();
7060                }
7061                Token::Ident(s) | Token::QuotedIdent(s)
7062                    if s.eq_ignore_ascii_case("end")
7063                        || s.eq_ignore_ascii_case("else")
7064                        || s.eq_ignore_ascii_case("elsif")
7065                        || s.eq_ignore_ascii_case("elseif")
7066                        || s.eq_ignore_ascii_case("exception")
7067                        || s.eq_ignore_ascii_case("when") =>
7068                {
7069                    // Final statement of the block without `;`.
7070                }
7071                other => {
7072                    return Err(self.err(alloc::format!(
7073                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7074                    )));
7075                }
7076            }
7077        }
7078    }
7079
7080    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7081        // RETURN keyword?
7082        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7083        {
7084            self.advance();
7085            return self.parse_plpgsql_return();
7086        }
7087        // v7.12.6 — IF block.
7088        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7089        {
7090            self.advance();
7091            return self.parse_plpgsql_if();
7092        }
7093        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7094        // Detected by peeking that token pos+3 is Ident("execute").
7095        if matches!(self.peek(), Token::For)
7096            && matches!(
7097                self.tokens.get(self.pos + 1),
7098                Some(Token::Ident(_) | Token::QuotedIdent(_))
7099            )
7100            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7101            && matches!(
7102                self.tokens.get(self.pos + 3),
7103                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7104            )
7105        {
7106            self.advance(); // FOR
7107            let var = self.expect_ident_like()?;
7108            self.advance(); // IN
7109            self.advance(); // EXECUTE
7110            // Prescan for LOOP at paren depth 0 so parse_expr stops
7111            // before the LOOP keyword (same trick as the bare-SELECT
7112            // ForQuery arm).
7113            let mut depth: i32 = 0;
7114            let mut loop_pos: Option<usize> = None;
7115            let mut scan = self.pos;
7116            while scan < self.tokens.len() {
7117                match self.tokens.get(scan) {
7118                    Some(Token::LParen) => depth += 1,
7119                    Some(Token::RParen) => depth -= 1,
7120                    Some(Token::Ident(s) | Token::QuotedIdent(s))
7121                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7122                    {
7123                        loop_pos = Some(scan);
7124                        break;
7125                    }
7126                    _ => {}
7127                }
7128                scan += 1;
7129            }
7130            let loop_pos = loop_pos.ok_or_else(|| {
7131                self.err(alloc::format!(
7132                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7133                ))
7134            })?;
7135            let saved_loop = self.tokens[loop_pos].clone();
7136            self.tokens[loop_pos] = Token::Semicolon;
7137            let expr_result = self.parse_expr(0);
7138            self.tokens[loop_pos] = saved_loop;
7139            let sql_expr = expr_result?;
7140            let loop_kw = self.expect_ident_like()?;
7141            if !loop_kw.eq_ignore_ascii_case("loop") {
7142                return Err(self.err(alloc::format!(
7143                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7144                )));
7145            }
7146            let body = self.parse_plpgsql_stmt_list_until_end()?;
7147            let end_kw = self.expect_ident_like()?;
7148            if !end_kw.eq_ignore_ascii_case("end") {
7149                return Err(self.err(alloc::format!(
7150                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7151                )));
7152            }
7153            let loop_kw2 = self.expect_ident_like()?;
7154            if !loop_kw2.eq_ignore_ascii_case("loop") {
7155                return Err(self.err(alloc::format!(
7156                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7157                )));
7158            }
7159            return Ok(PlPgSqlStmt::ForExecute {
7160                var,
7161                sql_expr,
7162                body,
7163            });
7164        }
7165        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7166        //
7167        // Two syntactic forms:
7168        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7169        //   FOR var IN (SELECT ...) LOOP ...
7170        //
7171        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7172        // the trailing `LOOP` keyword as a table alias, we prescan
7173        // forward to find LOOP at paren depth 0, splice a fake
7174        // Semicolon at that position (so SELECT parses cleanly),
7175        // then re-splice LOOP back in.
7176        //
7177        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7178        // LOOP directly — no scan required.
7179        if matches!(self.peek(), Token::For)
7180            && matches!(
7181                self.tokens.get(self.pos + 1),
7182                Some(Token::Ident(_) | Token::QuotedIdent(_))
7183            )
7184            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7185            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7186                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7187        {
7188            self.advance(); // FOR
7189            let var = self.expect_ident_like()?;
7190            // IN
7191            self.advance();
7192            let query = if matches!(self.peek(), Token::LParen) {
7193                // Paren-wrapped SELECT.
7194                self.advance();
7195                let inner = self.parse_select_stmt()?;
7196                let Statement::Select(q) = inner else {
7197                    return Err(self.err(alloc::format!(
7198                        "expected SELECT inside (…), got {:?}",
7199                        self.peek()
7200                    )));
7201                };
7202                if !matches!(self.peek(), Token::RParen) {
7203                    return Err(self.err(alloc::format!(
7204                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7205                        self.peek()
7206                    )));
7207                }
7208                self.advance();
7209                q
7210            } else {
7211                // Bare SELECT: prescan to find the LOOP boundary.
7212                let mut depth: i32 = 0;
7213                let mut loop_pos: Option<usize> = None;
7214                let mut scan = self.pos;
7215                while scan < self.tokens.len() {
7216                    match self.tokens.get(scan) {
7217                        Some(Token::LParen) => depth += 1,
7218                        Some(Token::RParen) => depth -= 1,
7219                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7220                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7221                        {
7222                            loop_pos = Some(scan);
7223                            break;
7224                        }
7225                        _ => {}
7226                    }
7227                    scan += 1;
7228                }
7229                let loop_pos = loop_pos.ok_or_else(|| {
7230                    self.err(alloc::format!(
7231                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7232                    ))
7233                })?;
7234                // Swap the LOOP token with a synthetic Semicolon so
7235                // parse_select_stmt stops there, then restore afterward.
7236                let saved_loop = self.tokens[loop_pos].clone();
7237                self.tokens[loop_pos] = Token::Semicolon;
7238                let parse_result = self.parse_select_stmt();
7239                self.tokens[loop_pos] = saved_loop;
7240                let inner = parse_result?;
7241                let Statement::Select(q) = inner else {
7242                    return Err(self.err(alloc::format!(
7243                        "expected SELECT after FOR <var> IN, got {:?}",
7244                        self.peek()
7245                    )));
7246                };
7247                q
7248            };
7249            let loop_kw = self.expect_ident_like()?;
7250            if !loop_kw.eq_ignore_ascii_case("loop") {
7251                return Err(self.err(alloc::format!(
7252                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7253                )));
7254            }
7255            let body = self.parse_plpgsql_stmt_list_until_end()?;
7256            let end_kw = self.expect_ident_like()?;
7257            if !end_kw.eq_ignore_ascii_case("end") {
7258                return Err(self.err(alloc::format!(
7259                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7260                )));
7261            }
7262            let loop_kw2 = self.expect_ident_like()?;
7263            if !loop_kw2.eq_ignore_ascii_case("loop") {
7264                return Err(self.err(alloc::format!(
7265                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7266                )));
7267            }
7268            return Ok(PlPgSqlStmt::ForQuery {
7269                var,
7270                query: Box::new(query),
7271                body,
7272            });
7273        }
7274        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7275        // FOR is a reserved keyword token (Token::For).
7276        if matches!(self.peek(), Token::For)
7277            && matches!(
7278                self.tokens.get(self.pos + 1),
7279                Some(Token::Ident(_) | Token::QuotedIdent(_))
7280            )
7281            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7282        {
7283            self.advance(); // FOR
7284            let var = self.expect_ident_like()?;
7285            if !matches!(self.peek(), Token::In) {
7286                return Err(self.err(alloc::format!(
7287                    "expected IN after FOR <var>, got {:?}",
7288                    self.peek()
7289                )));
7290            }
7291            self.advance();
7292            let reverse = matches!(
7293                self.peek(),
7294                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7295            );
7296            if reverse {
7297                self.advance();
7298            }
7299            let start = self.parse_expr(0)?;
7300            if !matches!(self.peek(), Token::DotDot) {
7301                return Err(self.err(alloc::format!(
7302                    "expected '..' between FOR loop bounds, got {:?}",
7303                    self.peek()
7304                )));
7305            }
7306            self.advance();
7307            let end = self.parse_expr(0)?;
7308            let loop_kw = self.expect_ident_like()?;
7309            if !loop_kw.eq_ignore_ascii_case("loop") {
7310                return Err(self.err(alloc::format!(
7311                    "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7312                )));
7313            }
7314            let body = self.parse_plpgsql_stmt_list_until_end()?;
7315            let end_kw = self.expect_ident_like()?;
7316            if !end_kw.eq_ignore_ascii_case("end") {
7317                return Err(self.err(alloc::format!(
7318                    "expected END LOOP after FOR body, got {end_kw:?}"
7319                )));
7320            }
7321            let loop_kw2 = self.expect_ident_like()?;
7322            if !loop_kw2.eq_ignore_ascii_case("loop") {
7323                return Err(self.err(alloc::format!(
7324                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7325                )));
7326            }
7327            return Ok(PlPgSqlStmt::ForRange {
7328                var,
7329                start,
7330                end,
7331                reverse,
7332                body,
7333            });
7334        }
7335        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7336        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7337        {
7338            self.advance();
7339            let body = self.parse_plpgsql_stmt_list_until_end()?;
7340            let end_kw = self.expect_ident_like()?;
7341            if !end_kw.eq_ignore_ascii_case("end") {
7342                return Err(self.err(alloc::format!(
7343                    "expected END LOOP after LOOP body, got {end_kw:?}"
7344                )));
7345            }
7346            let loop_kw = self.expect_ident_like()?;
7347            if !loop_kw.eq_ignore_ascii_case("loop") {
7348                return Err(self.err(alloc::format!(
7349                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7350                )));
7351            }
7352            return Ok(PlPgSqlStmt::Loop { body });
7353        }
7354        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7355        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7356        {
7357            self.advance();
7358            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7359            {
7360                self.advance();
7361                Some(self.parse_expr(0)?)
7362            } else {
7363                None
7364            };
7365            return Ok(PlPgSqlStmt::Exit { when });
7366        }
7367        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7368        // already-parsed Statement or a runtime-computed SQL string.
7369        // The disambiguator vs the extended-query-protocol `EXECUTE
7370        // <stmt_name>` (which is a top-level Statement, not a
7371        // plpgsql line) is that inside a DO block / trigger body the
7372        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7373        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7374        {
7375            self.advance();
7376            let sql = self.parse_expr(0)?;
7377            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7378        }
7379        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7380        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7381        {
7382            self.advance();
7383            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7384            {
7385                self.advance();
7386                Some(self.parse_expr(0)?)
7387            } else {
7388                None
7389            };
7390            return Ok(PlPgSqlStmt::Continue { when });
7391        }
7392        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7393        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7394        {
7395            self.advance();
7396            let condition = self.parse_expr(0)?;
7397            let loop_kw = self.expect_ident_like()?;
7398            if !loop_kw.eq_ignore_ascii_case("loop") {
7399                return Err(self.err(alloc::format!(
7400                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7401                )));
7402            }
7403            let body = self.parse_plpgsql_stmt_list_until_end()?;
7404            // Expect END LOOP.
7405            let end_kw = self.expect_ident_like()?;
7406            if !end_kw.eq_ignore_ascii_case("end") {
7407                return Err(self.err(alloc::format!(
7408                    "expected END LOOP after WHILE body, got {end_kw:?}"
7409                )));
7410            }
7411            let loop_kw2 = self.expect_ident_like()?;
7412            if !loop_kw2.eq_ignore_ascii_case("loop") {
7413                return Err(self.err(alloc::format!(
7414                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7415                )));
7416            }
7417            return Ok(PlPgSqlStmt::While { condition, body });
7418        }
7419        // v7.12.6 — RAISE.
7420        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7421        {
7422            self.advance();
7423            return self.parse_plpgsql_raise();
7424        }
7425        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7426        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7427        {
7428            self.advance();
7429            let condition = self.parse_expr(0)?;
7430            let message = if matches!(self.peek(), Token::Comma) {
7431                self.advance();
7432                Some(self.parse_expr(0)?)
7433            } else {
7434                None
7435            };
7436            return Ok(PlPgSqlStmt::Assert { condition, message });
7437        }
7438        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7439        //   "PERFORM is equivalent to SELECT but discards the
7440        //    result." Side effects (function calls, RAISE inside
7441        //    SQL functions, etc.) still execute. We desugar to
7442        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7443        //    existing embedded-statement path handles execution +
7444        //    result-discard cleanly. The result is naturally
7445        //    discarded because EmbeddedSql doesn't propagate row
7446        //    sets back to the plpgsql interpreter.
7447        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7448        {
7449            self.advance();
7450            // Splice a synthetic Token::Select into the stream at
7451            // the current position so parse_select_stmt parses the
7452            // remainder as a normal SELECT body. Token-stream
7453            // surgery mirrors the try_parse_plpgsql_select_into
7454            // pattern used for SELECT … INTO desugaring.
7455            self.tokens.insert(self.pos, Token::Select);
7456            let select = self.parse_select_stmt()?;
7457            let Statement::Select(s) = select else {
7458                return Err(self.err(alloc::format!(
7459                    "expected SELECT body after PERFORM, got {:?}",
7460                    self.peek()
7461                )));
7462            };
7463            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7464        }
7465        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7466        // plpgsql-specific shape (mailrs round-10 migrate-042).
7467        // PG's SELECT INTO at top-level SQL would CREATE a new
7468        // table; inside plpgsql it ASSIGNS the query result to
7469        // a local variable. We detect the INTO at paren-depth
7470        // 0 between SELECT and the statement boundary; if
7471        // found, split the token stream into "pre-INTO
7472        // projection" + "var" + "post-INTO FROM/WHERE…" and
7473        // rebuild as a SelectInto with a regular SELECT body
7474        // (no INTO clause).
7475        if matches!(self.peek(), Token::Select)
7476            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7477        {
7478            return Ok(PlPgSqlStmt::SelectInto {
7479                var: var_name,
7480                body: Box::new(select_body),
7481            });
7482        }
7483        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7484        // SELECT can appear directly inside a trigger body; we
7485        // recurse into the regular Statement parser, which will
7486        // stop at the trailing `;` (which our caller then
7487        // consumes).
7488        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7489        // also embed ALTER / CREATE / DROP statements; route
7490        // those through the same parser so the DO body parses
7491        // cleanly.
7492        if matches!(self.peek(), Token::Insert)
7493            || matches!(self.peek(), Token::Select)
7494            || matches!(self.peek(), Token::Create)
7495            || matches!(self.peek(), Token::Drop)
7496            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7497                if s.eq_ignore_ascii_case("update")
7498                    || s.eq_ignore_ascii_case("delete")
7499                    || s.eq_ignore_ascii_case("alter"))
7500        {
7501            let stmt = self.parse_one_statement()?;
7502            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7503        }
7504        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7505        // followed by `:=` and an expression.
7506        let target = self.parse_plpgsql_assign_target()?;
7507        // PL/pgSQL assignment uses `:=`. The lexer represents
7508        // this as a colon followed by `=`; check both shapes.
7509        match self.peek() {
7510            Token::ColonEq => {
7511                self.advance();
7512            }
7513            Token::Colon => {
7514                self.advance();
7515                if !matches!(self.peek(), Token::Eq) {
7516                    return Err(self.err(alloc::format!(
7517                        "expected := after plpgsql assign target, got `:` then {:?}",
7518                        self.peek()
7519                    )));
7520                }
7521                self.advance();
7522            }
7523            other => {
7524                return Err(self.err(alloc::format!(
7525                    "expected := after plpgsql assign target, got {other:?}"
7526                )));
7527            }
7528        }
7529        let value = self.parse_expr(0)?;
7530        Ok(PlPgSqlStmt::Assign { target, value })
7531    }
7532
7533    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7534    /// [ELSE body] END IF`. `IF` keyword already consumed.
7535    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7536        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7537        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7538        loop {
7539            // <expr> THEN
7540            let cond = self.parse_expr(0)?;
7541            let then_kw = self.expect_ident_like()?;
7542            if !then_kw.eq_ignore_ascii_case("then") {
7543                return Err(self.err(alloc::format!(
7544                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7545                )));
7546            }
7547            let body = self.parse_plpgsql_stmt_list_until_end()?;
7548            branches.push((cond, body));
7549            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7550            match self.peek() {
7551                Token::Ident(s) | Token::QuotedIdent(s)
7552                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7553                {
7554                    self.advance();
7555                    continue;
7556                }
7557                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7558                    self.advance();
7559                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7560                    break;
7561                }
7562                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7563                    break;
7564                }
7565                other => {
7566                    return Err(self.err(alloc::format!(
7567                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7568                    )));
7569                }
7570            }
7571        }
7572        // Expect `END IF` (the END keyword is the one we're
7573        // looking at right now).
7574        let end_kw = self.expect_ident_like()?;
7575        if !end_kw.eq_ignore_ascii_case("end") {
7576            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7577        }
7578        let if_kw = self.expect_ident_like()?;
7579        if !if_kw.eq_ignore_ascii_case("if") {
7580            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7581        }
7582        Ok(PlPgSqlStmt::If {
7583            branches,
7584            else_branch,
7585        })
7586    }
7587
7588    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7589    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7590    /// is already consumed.
7591    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7592        let lvl_ident = self.expect_ident_like()?;
7593        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7594            "notice" => RaiseLevel::Notice,
7595            "warning" => RaiseLevel::Warning,
7596            "info" => RaiseLevel::Info,
7597            "log" => RaiseLevel::Log,
7598            "debug" => RaiseLevel::Debug,
7599            "exception" => RaiseLevel::Exception,
7600            other => {
7601                return Err(self.err(alloc::format!(
7602                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7603                )));
7604            }
7605        };
7606        // Message: required for v7.12.6. PG accepts a bare
7607        // RAISE-rethrow form (no message), reserved for future
7608        // RAISE-no-args support.
7609        let Token::String(msg) = self.peek() else {
7610            return Err(self.err(alloc::format!(
7611                "expected RAISE message string, got {:?}",
7612                self.peek()
7613            )));
7614        };
7615        let message = msg.clone();
7616        self.advance();
7617        // Optional comma-separated args (PG `%` format substitution).
7618        let mut args: Vec<Expr> = Vec::new();
7619        while matches!(self.peek(), Token::Comma) {
7620            self.advance();
7621            args.push(self.parse_expr(0)?);
7622        }
7623        Ok(PlPgSqlStmt::Raise {
7624            level,
7625            message,
7626            args,
7627        })
7628    }
7629
7630    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7631    /// <projection> INTO <var> [FROM …]` (mailrs round-10
7632    /// migrate-042). Returns `(rebuilt_select_without_into,
7633    /// var_name)` when the pattern matches; `None` for
7634    /// regular SELECTs (those go through the embedded-SQL
7635    /// path). Token-stream surgery so the rebuilt SELECT
7636    /// parses through the regular `parse_select_stmt`.
7637    #[allow(clippy::too_many_lines)]
7638    fn try_parse_plpgsql_select_into(
7639        &mut self,
7640    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7641        // Scan forward from `self.pos + 1` (past Token::Select)
7642        // for Token::Into at paren-depth 0, stopping at the
7643        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7644        // end the plpgsql statement.
7645        let start = self.pos;
7646        let mut into_pos: Option<usize> = None;
7647        let mut depth: i32 = 0;
7648        let mut i = start + 1;
7649        while i < self.tokens.len() {
7650            match &self.tokens[i] {
7651                Token::LParen => depth += 1,
7652                Token::RParen => depth -= 1,
7653                Token::Semicolon if depth == 0 => break,
7654                Token::Ident(s)
7655                    if depth == 0
7656                        && (s.eq_ignore_ascii_case("end")
7657                            || s.eq_ignore_ascii_case("else")
7658                            || s.eq_ignore_ascii_case("elsif")) =>
7659                {
7660                    break;
7661                }
7662                Token::Into if depth == 0 => {
7663                    into_pos = Some(i);
7664                    break;
7665                }
7666                _ => {}
7667            }
7668            i += 1;
7669        }
7670        let Some(into_at) = into_pos else {
7671            return Ok(None);
7672        };
7673        // The token immediately after INTO must be the target
7674        // var ident; anything else (e.g. INSERT INTO table)
7675        // ruled out by the depth-0 check above. Capture it.
7676        let var = match self.tokens.get(into_at + 1) {
7677            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7678            other => {
7679                return Err(self.err(alloc::format!(
7680                    "expected variable name after SELECT … INTO, got {other:?}"
7681                )));
7682            }
7683        };
7684        // Find the end of the plpgsql SELECT INTO statement —
7685        // same boundary rules as the depth-0 scan above.
7686        let mut end = into_at + 2;
7687        let mut depth2: i32 = 0;
7688        while end < self.tokens.len() {
7689            match &self.tokens[end] {
7690                Token::LParen => depth2 += 1,
7691                Token::RParen => depth2 -= 1,
7692                Token::Semicolon if depth2 == 0 => break,
7693                Token::Ident(s)
7694                    if depth2 == 0
7695                        && (s.eq_ignore_ascii_case("end")
7696                            || s.eq_ignore_ascii_case("else")
7697                            || s.eq_ignore_ascii_case("elsif")) =>
7698                {
7699                    break;
7700                }
7701                _ => {}
7702            }
7703            end += 1;
7704        }
7705        // Rebuild a token stream that represents the SELECT
7706        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7707        // post-var tokens up to statement end]. Run the
7708        // regular `parse_select_stmt` against it.
7709        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7710        for j in start..into_at {
7711            rebuilt.push(self.tokens[j].clone());
7712        }
7713        for j in (into_at + 2)..end {
7714            rebuilt.push(self.tokens[j].clone());
7715        }
7716        rebuilt.push(Token::Eof);
7717        let saved_pos = self.pos;
7718        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7719        self.pos = 0;
7720        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7721        if !matches!(self.peek(), Token::Select) {
7722            self.tokens = saved_tokens;
7723            self.pos = saved_pos;
7724            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7725        }
7726        let sel = self.parse_select_stmt();
7727        self.tokens = saved_tokens;
7728        self.pos = end;
7729        let sel = sel?;
7730        let Statement::Select(body) = sel else {
7731            return Err(self.err(alloc::format!(
7732                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7733            )));
7734        };
7735        Ok(Some((body, var)))
7736    }
7737
7738    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7739        // v7.16.1 — read the head token DIRECTLY rather than
7740        // via `expect_ident_like`. The v7.14.0 schema-qualifier
7741        // strip (`public.t` → `t`) inside `expect_ident_like`
7742        // greedily consumes any `ident . ident` pair, which
7743        // silently turned every `NEW.col := …` /
7744        // `OLD.col := …` plpgsql assignment into a Local("col")
7745        // assignment — the head "new"/"old" was eaten as if it
7746        // were a schema name and the Dot was consumed too, so
7747        // this function's own `peek() == Token::Dot` check
7748        // below never fired. Every BEFORE trigger that rewrote
7749        // a NEW cell was a silent no-op for two major releases
7750        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7751        // gate failures were investigated as v7.16.1 backlog.
7752        let head = match self.advance() {
7753            Token::Ident(s) | Token::QuotedIdent(s) => s,
7754            other => {
7755                return Err(self.err(alloc::format!(
7756                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7757                )));
7758            }
7759        };
7760        if matches!(self.peek(), Token::Dot) {
7761            self.advance();
7762            let col = self.expect_ident_like()?;
7763            if head.eq_ignore_ascii_case("new") {
7764                return Ok(AssignTarget::NewColumn(col));
7765            }
7766            if head.eq_ignore_ascii_case("old") {
7767                return Ok(AssignTarget::OldColumn(col));
7768            }
7769            return Err(self.err(alloc::format!(
7770                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7771                 got {head:?}.<col>"
7772            )));
7773        }
7774        Ok(AssignTarget::Local(head))
7775    }
7776
7777    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7778        // RETURN NEW / OLD / NULL — bare-ident forms.
7779        match self.peek() {
7780            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7781                self.advance();
7782                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7783            }
7784            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7785                self.advance();
7786                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7787            }
7788            Token::Null => {
7789                self.advance();
7790                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7791            }
7792            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7793            // per PL/pgSQL convention.
7794            Token::Semicolon => {
7795                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7796            }
7797            _ => {}
7798        }
7799        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7800        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7801        // caller-visible effect (blocks don't return sets), so we
7802        // desugar it identically to PERFORM: parse the SELECT (or
7803        // EXECUTE dynamic) as embedded SQL that runs for side
7804        // effects and discards the result. RETURN NEXT <expr>
7805        // (single-row accumulator) queues with v7.40 SETOF function
7806        // infrastructure.
7807        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7808        // and keep going.
7809        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7810        {
7811            self.advance();
7812            let e = self.parse_expr(0)?;
7813            return Ok(PlPgSqlStmt::ReturnNext(e));
7814        }
7815        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7816        {
7817            self.advance();
7818            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7819            // rows go to the set, like the static form. It used to desugar to a
7820            // bare ExecuteDynamic, whose result was DISCARDED.
7821            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7822            {
7823                self.advance();
7824                let sql = self.parse_expr(0)?;
7825                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7826            }
7827            // Bare RETURN QUERY <select>. If the current token is
7828            // not already SELECT (e.g., the user wrote `RETURN QUERY
7829            // <projection> FROM ...` in a shorthand — rare but PG
7830            // accepts a bare projection here), splice one in. Same
7831            // trick as PERFORM.
7832            if !matches!(self.peek(), Token::Select) {
7833                self.tokens.insert(self.pos, Token::Select);
7834            }
7835            let select = self.parse_select_stmt()?;
7836            let Statement::Select(s) = select else {
7837                return Err(self.err(alloc::format!(
7838                    "expected SELECT body after RETURN QUERY, got {:?}",
7839                    self.peek()
7840                )));
7841            };
7842            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7843            // to an embedded side-effect SELECT whose rows were DISCARDED, which
7844            // in a SETOF function is the entire answer thrown away.
7845            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7846        }
7847        // Fall through: parse a full expression.
7848        let e = self.parse_expr(0)?;
7849        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7850    }
7851
7852    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7853        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7854        // are ident-shaped (the parser keys off case-insensitive
7855        // match — same shape used by the top-level Update / Delete
7856        // dispatchers at parse_one_statement).
7857        if matches!(self.peek(), Token::Insert) {
7858            self.advance();
7859            return Ok(TriggerEvent::Insert);
7860        }
7861        match self.peek() {
7862            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7863                self.advance();
7864                Ok(TriggerEvent::Update)
7865            }
7866            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7867                self.advance();
7868                Ok(TriggerEvent::Delete)
7869            }
7870            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7871                self.advance();
7872                Ok(TriggerEvent::Truncate)
7873            }
7874            other => Err(self.err(alloc::format!(
7875                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
7876            ))),
7877        }
7878    }
7879
7880    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
7881    ///   - (no clause) → implicit `FOR ALL TABLES`
7882    ///   - `FOR ALL TABLES`
7883    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
7884    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
7885    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
7886    ///     REJECTS the bare plural (`invalid publication object list`,
7887    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
7888    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
7889    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
7890        let name = self.expect_ident_or_string()?;
7891        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
7892        // shape so existing publications keep parsing identically.
7893        let scope = if matches!(self.peek(), Token::For) {
7894            self.advance();
7895            if matches!(self.peek(), Token::All) {
7896                self.advance();
7897                if !matches!(self.peek(), Token::Tables) {
7898                    return Err(self.err(format!(
7899                        "expected TABLES after FOR ALL, got {:?}",
7900                        self.peek()
7901                    )));
7902                }
7903                self.advance();
7904                if matches!(self.peek(), Token::Except) {
7905                    self.advance();
7906                    let tables = self.parse_publication_table_list()?;
7907                    PublicationScope::AllTablesExcept(tables)
7908                } else {
7909                    PublicationScope::AllTables
7910                }
7911            } else if matches!(self.peek(), Token::Table) {
7912                self.advance();
7913                let tables = self.parse_publication_table_list()?;
7914                PublicationScope::ForTables(tables)
7915            } else if matches!(self.peek(), Token::Tables) {
7916                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
7917                // plural (`FOR TABLES t`) is REJECTED (`invalid
7918                // publication object list`); TABLES only pairs with
7919                // `IN SCHEMA`. The old arm accepted it on an
7920                // unverifiable "PG 19 accepts both" claim.
7921                self.advance();
7922                if !matches!(self.peek(), Token::In) {
7923                    return Err(self.err(alloc::string::String::from(
7924                        "invalid publication object list",
7925                    )));
7926                }
7927                self.advance();
7928                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
7929                    return Err(self.err(format!(
7930                        "expected SCHEMA after FOR TABLES IN, got {:?}",
7931                        self.peek()
7932                    )));
7933                }
7934                self.advance();
7935                let schema = self.expect_ident_or_string()?;
7936                PublicationScope::TablesInSchema(schema)
7937            } else {
7938                return Err(self.err(format!(
7939                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
7940                    self.peek()
7941                )));
7942            }
7943        } else {
7944            PublicationScope::AllTables
7945        };
7946        Ok(Statement::CreatePublication(CreatePublicationStatement {
7947            name,
7948            scope,
7949        }))
7950    }
7951
7952    /// v6.1.3 — Comma-separated identifier list for the publication
7953    /// FOR-clause. Requires at least one entry; empty list is a
7954    /// parse error (PG behaviour). Quoted idents are accepted; the
7955    /// names round-trip through `Display` as `quote_ident(name)`.
7956    ///
7957    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
7958    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
7959    /// pg_dump output. SPG's publication state today is per-table
7960    /// only (matching the pre-PG-15 surface); the col list + WHERE
7961    /// are parsed so dumps load through and the table name reaches
7962    /// `PublicationScope::ForTables`, but the filter is not enforced
7963    /// at publish time. Re-open when a customer dogfood gate
7964    /// requires per-row-filter or column-subset publish semantics
7965    /// (which gates on persistent slot state landing first, 21.12).
7966    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
7967        let first = self.parse_publication_table_entry()?;
7968        let mut out = alloc::vec![first];
7969        while matches!(self.peek(), Token::Comma) {
7970            self.advance();
7971            out.push(self.parse_publication_table_entry()?);
7972        }
7973        Ok(out)
7974    }
7975
7976    /// One table entry inside a FOR TABLE clause:
7977    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
7978    /// Returns just the table name; the column list + WHERE predicate
7979    /// are consumed and discarded per the parse-accept-discard
7980    /// commitment above.
7981    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
7982        let name = self.expect_ident_like()?;
7983        // Optional column list — `(col, col, …)`.
7984        if matches!(self.peek(), Token::LParen) {
7985            self.advance();
7986            // Empty parens are a PG error too; require ≥ 1 column.
7987            let _ = self.expect_ident_like()?;
7988            while matches!(self.peek(), Token::Comma) {
7989                self.advance();
7990                let _ = self.expect_ident_like()?;
7991            }
7992            if !matches!(self.peek(), Token::RParen) {
7993                return Err(self.err(alloc::format!(
7994                    "expected ')' to close publication column list, got {:?}",
7995                    self.peek()
7996                )));
7997            }
7998            self.advance();
7999        }
8000        // Optional row filter — `WHERE (predicate)`.
8001        if matches!(self.peek(), Token::Where) {
8002            self.advance();
8003            if !matches!(self.peek(), Token::LParen) {
8004                return Err(self.err(alloc::format!(
8005                    "expected '(' after WHERE in publication row filter, got {:?}",
8006                    self.peek()
8007                )));
8008            }
8009            self.advance();
8010            let _ = self.parse_expr(0)?;
8011            if !matches!(self.peek(), Token::RParen) {
8012                return Err(self.err(alloc::format!(
8013                    "expected ')' to close publication WHERE filter, got {:?}",
8014                    self.peek()
8015                )));
8016            }
8017            self.advance();
8018        }
8019        Ok(name)
8020    }
8021
8022    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8023    ///                 CONNECTION '<conn>'
8024    ///                 PUBLICATION <pub> [, <pub> ...]`.
8025    ///
8026    /// The clause order is fixed (CONNECTION first, then
8027    /// PUBLICATION) to match PG. No WITH-options accepted in
8028    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8029    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8030        let name = self.expect_ident_or_string()?;
8031        if !matches!(self.peek(), Token::Connection) {
8032            return Err(self.err(format!(
8033                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8034                self.peek()
8035            )));
8036        }
8037        self.advance();
8038        let conn_str = self.expect_string_literal()?;
8039        if !matches!(self.peek(), Token::Publication) {
8040            return Err(self.err(format!(
8041                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8042                self.peek()
8043            )));
8044        }
8045        self.advance();
8046        // Reuse the publication FOR-list parser shape: at least one
8047        // identifier, comma-separated.
8048        let first = self.expect_ident_like()?;
8049        let mut publications = alloc::vec![first];
8050        while matches!(self.peek(), Token::Comma) {
8051            self.advance();
8052            publications.push(self.expect_ident_like()?);
8053        }
8054        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8055            name,
8056            conn_str,
8057            publications,
8058        }))
8059    }
8060
8061    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8062    /// All keywords after `WAIT` are bare idents in v6.1.x; no
8063    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8064    /// that fit `u64`.
8065    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8066    /// qualifier is a *namespace* the app owns (`app.user_id`,
8067    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8068    /// to discard. So parse the raw segments here instead of
8069    /// `expect_ident_like`, which strips a leading `schema.` qualifier
8070    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8071    /// a single segment and round-trip unchanged.
8072    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8073        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8074        loop {
8075            let seg = match self.advance() {
8076                Token::Ident(s) | Token::QuotedIdent(s) => s,
8077                other if unreserved_keyword_text(&other).is_some() => {
8078                    unreserved_keyword_text(&other).unwrap()
8079                }
8080                other => {
8081                    return Err(ParseError {
8082                        message: format!("expected parameter name, got {other:?}"),
8083                        token_pos: self.consumed_pos(),
8084                    });
8085                }
8086            };
8087            parts.push(seg);
8088            if matches!(self.peek(), Token::Dot) {
8089                self.advance();
8090                continue;
8091            }
8092            break;
8093        }
8094        Ok(parts.join(".").to_ascii_lowercase())
8095    }
8096
8097    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8098        Self::parse_set_value_inner(self)
8099    }
8100
8101    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8102        match self.advance() {
8103            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8104            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8105                Ok(crate::ast::SetValue::Default)
8106            }
8107            Token::Ident(s) | Token::QuotedIdent(s) => {
8108                let mut accum = s;
8109                while matches!(self.peek(), Token::Dot) {
8110                    self.advance();
8111                    let next = self.expect_ident_like()?;
8112                    accum.push('.');
8113                    accum.push_str(&next);
8114                }
8115                Ok(crate::ast::SetValue::Ident(accum))
8116            }
8117            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8118            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8119            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8120            // spellings that lex as keyword tokens, not idents:
8121            // `SET standard_conforming_strings = on` is in every
8122            // pg_dump preamble (`off` already lexes as an ident).
8123            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8124            // DEFAULT lexes as its keyword token, so the ident arm above
8125            // never saw it and the everyday reset form was a syntax error.
8126            Token::Default => Ok(crate::ast::SetValue::Default),
8127            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8128            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8129            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8130            // v7.14.0 — MySQL session/user variable RHS
8131            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8132            // Wrap as Ident so the SET handler can record it; the
8133            // engine treats `@VAR` / `@@VAR` values as opaque
8134            // strings.
8135            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8136            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8137            // is the common MySQL preamble shape. Allow a `+` or
8138            // `-` prefix on negative numerics for parity with PG
8139            // (some param defaults are negative).
8140            Token::Minus => match self.advance() {
8141                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8142                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8143                other => Err(self.err(format!(
8144                    "expected numeric after `-` in SET value, got {other:?}"
8145                ))),
8146            },
8147            other => Err(self.err(format!(
8148                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8149            ))),
8150        }
8151    }
8152
8153    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8154    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8155    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8156    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8157    /// present). Modes are comma-separated per PG; SPG also
8158    /// accepts space-separated for tolerance. READ ONLY / WRITE
8159    /// / DEFERRABLE are parsed-and-ignored (recorded for future
8160    /// surface but not behaviorally honoured today).
8161    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8162    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8163    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8164    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8165    /// session default rather than forcing READ COMMITTED.
8166    fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8167        let mut level = IsolationLevel::default();
8168        let mut have_level = false;
8169        loop {
8170            // ISOLATION LEVEL …
8171            let saw_isolation =
8172                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8173            if saw_isolation {
8174                self.advance(); // ISOLATION
8175                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8176                    return Err(self.err(alloc::format!(
8177                        "expected LEVEL after ISOLATION, got {:?}",
8178                        self.peek()
8179                    )));
8180                }
8181                self.advance(); // LEVEL
8182                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8183                let w1 = self
8184                    .expect_ident_like()
8185                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8186                let lc = w1.to_ascii_lowercase();
8187                level = match lc.as_str() {
8188                    "serializable" => IsolationLevel::Serializable,
8189                    "repeatable" => {
8190                        // Expect READ
8191                        let w2 = self
8192                            .expect_ident_like()
8193                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8194                        if !w2.eq_ignore_ascii_case("read") {
8195                            return Err(self.err(alloc::format!(
8196                                "expected READ after REPEATABLE, got {w2:?}"
8197                            )));
8198                        }
8199                        IsolationLevel::RepeatableRead
8200                    }
8201                    "read" => {
8202                        let w2 = self
8203                            .expect_ident_like()
8204                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8205                        match w2.to_ascii_lowercase().as_str() {
8206                            "committed" => IsolationLevel::ReadCommitted,
8207                            "uncommitted" => IsolationLevel::ReadUncommitted,
8208                            other => {
8209                                return Err(self.err(alloc::format!(
8210                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8211                                )));
8212                            }
8213                        }
8214                    }
8215                    other => {
8216                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8217                    }
8218                };
8219                have_level = true;
8220            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8221                // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8222                self.advance();
8223                match self.peek().clone() {
8224                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8225                        self.advance();
8226                    }
8227                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8228                        self.advance();
8229                    }
8230                    other => {
8231                        return Err(self.err(alloc::format!(
8232                            "expected ONLY or WRITE after READ, got {other:?}"
8233                        )));
8234                    }
8235                }
8236            } else if matches!(self.peek(), Token::Not) {
8237                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8238                self.advance();
8239                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8240                    return Err(self.err(alloc::format!(
8241                        "expected DEFERRABLE after NOT, got {:?}",
8242                        self.peek()
8243                    )));
8244                }
8245                self.advance();
8246            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8247            {
8248                self.advance();
8249            } else {
8250                break;
8251            }
8252            // Optional comma between modes.
8253            if matches!(self.peek(), Token::Comma) {
8254                self.advance();
8255            }
8256        }
8257        Ok(have_level.then_some(level))
8258    }
8259
8260    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8261        // FOR is a v6.1.2-reserved keyword (Token::For). The
8262        // other two are bare idents — they've never needed lexer
8263        // support and we keep it that way.
8264        if !matches!(self.peek(), Token::For) {
8265            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8266        }
8267        self.advance();
8268        self.expect_keyword_ident("wal")?;
8269        self.expect_keyword_ident("position")?;
8270        let pos = self.expect_u64_literal()?;
8271        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8272        {
8273            self.advance();
8274            self.expect_keyword_ident("timeout")?;
8275            Some(self.expect_u64_literal()?)
8276        } else {
8277            None
8278        };
8279        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8280    }
8281
8282    /// v6.1.7 helper — consume a `Token::Integer` and check it
8283    /// fits `u64`. WAL positions and millisecond timeouts are
8284    /// non-negative.
8285    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8286        match self.advance() {
8287            Token::Integer(n) if n >= 0 => Ok(n as u64),
8288            Token::Integer(n) => Err(ParseError {
8289                message: format!("expected non-negative integer, got {n}"),
8290                token_pos: self.consumed_pos(),
8291            }),
8292            other => Err(ParseError {
8293                message: format!("expected integer literal, got {other:?}"),
8294                token_pos: self.consumed_pos(),
8295            }),
8296        }
8297    }
8298
8299    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8300    /// ROLE '<role>' (defaults to readonly). All string slots accept
8301    /// either a quoted ident or a quoted string literal.
8302    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8303    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8304    ///
8305    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8306    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8307    /// wire role) still parses — it is a different axis from the PG attributes.
8308    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8309    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8310    /// or RESET, so the plain attribute forms keep their old path.
8311    fn peeks_db_role_setting(&self) -> bool {
8312        let mut i = self.pos + 1; // past the object's name
8313        let word = |p: usize| -> Option<String> {
8314            match self.tokens.get(p) {
8315                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8316                Some(Token::In) => Some(String::from("in")),
8317                _ => None,
8318            }
8319        };
8320        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8321            i += 3; // IN DATABASE <name>
8322        }
8323        matches!(word(i).as_deref(), Some("set" | "reset"))
8324    }
8325
8326    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8327        use crate::ast::SetDbRoleSettingStatement;
8328        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8329        // identifier, so the ordinary name reader refuses it. Same trap
8330        // as TABLE / INDEX / FULL / DEFAULT before it.
8331        let name = if matches!(self.peek(), Token::All) {
8332            self.advance();
8333            String::from("all")
8334        } else {
8335            self.expect_ident_or_string()?
8336        };
8337        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8338        let all = name.eq_ignore_ascii_case("all");
8339        let (mut database, mut role) = if is_database {
8340            (Some(name), None)
8341        } else if all {
8342            (None, None)
8343        } else {
8344            (None, Some(name))
8345        };
8346        if matches!(self.peek(), Token::In) {
8347            self.advance();
8348            self.advance(); // DATABASE
8349            database = Some(self.expect_ident_or_string()?);
8350        }
8351        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8352        self.advance(); // SET | RESET
8353        if resetting && matches!(self.peek(), Token::All) {
8354            self.advance();
8355            self.consume_until_statement_boundary();
8356            return Ok(Statement::SetDbRoleSetting(Box::new(
8357                SetDbRoleSettingStatement {
8358                    database,
8359                    role,
8360                    param: None,
8361                    value: None,
8362                },
8363            )));
8364        }
8365        let param = self.expect_ident_like()?;
8366        let value = if resetting {
8367            None
8368        } else {
8369            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8370            // KEYWORD, so the ident-only check missed it and consumed
8371            // the word itself as the value — the same trap as ALL, one
8372            // clause over.
8373            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8374                self.advance();
8375            }
8376            Some(self.take_guc_value())
8377        };
8378        self.consume_until_statement_boundary();
8379        Ok(Statement::SetDbRoleSetting(Box::new(
8380            SetDbRoleSettingStatement {
8381                database,
8382                role,
8383                param: Some(param),
8384                value,
8385            },
8386        )))
8387    }
8388
8389    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8390    /// a quoted literal loses its quotes, a bare word or number does not.
8391    fn take_guc_value(&mut self) -> String {
8392        match self.advance() {
8393            Token::String(s) => s,
8394            Token::Integer(n) => format!("{n}"),
8395            Token::Float(f) => format!("{f}"),
8396            Token::Ident(s) | Token::QuotedIdent(s) => s,
8397            other => format!("{other:?}"),
8398        }
8399    }
8400
8401    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8402        let name = self.expect_ident_or_string()?;
8403        if self.peek_keyword_ident("with") {
8404            self.advance();
8405        }
8406        let mut password = String::new();
8407        let mut role = String::new();
8408        let mut login: Option<bool> = None;
8409        let mut inherit: Option<bool> = None;
8410        let mut superuser: Option<bool> = None;
8411        // Not a `while let`: the pattern would borrow `self` across the
8412        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8413        #[allow(clippy::while_let_loop)]
8414        loop {
8415            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8416                break;
8417            };
8418            match w.to_ascii_lowercase().as_str() {
8419                "password" => {
8420                    self.advance();
8421                    password = self.expect_string_literal()?;
8422                }
8423                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8424                // is the same slot.
8425                "encrypted" => {
8426                    self.advance();
8427                    self.expect_keyword_ident("password")?;
8428                    password = self.expect_string_literal()?;
8429                }
8430                "login" => {
8431                    self.advance();
8432                    login = Some(true);
8433                }
8434                "nologin" => {
8435                    self.advance();
8436                    login = Some(false);
8437                }
8438                "inherit" => {
8439                    self.advance();
8440                    inherit = Some(true);
8441                }
8442                "noinherit" => {
8443                    self.advance();
8444                    inherit = Some(false);
8445                }
8446                "superuser" => {
8447                    self.advance();
8448                    superuser = Some(true);
8449                }
8450                "nosuperuser" => {
8451                    self.advance();
8452                    superuser = Some(false);
8453                }
8454                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8455                "role" => {
8456                    self.advance();
8457                    role = self.expect_string_literal()?;
8458                }
8459                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8460                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8461                // accepted and ignored so a pg_dump role block restores. They
8462                // gate capabilities SPG does not have.
8463                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8464                | "noreplication" | "bypassrls" | "nobypassrls" => {
8465                    self.advance();
8466                }
8467                "connection" => {
8468                    self.advance();
8469                    self.expect_keyword_ident("limit")?;
8470                    self.advance(); // the number
8471                }
8472                "valid" => {
8473                    self.advance();
8474                    self.expect_keyword_ident("until")?;
8475                    self.expect_string_literal()?;
8476                }
8477                _ => break,
8478            }
8479        }
8480        if role.is_empty() {
8481            role = "readonly".to_string();
8482        }
8483        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8484            name,
8485            password,
8486            role,
8487            login,
8488            inherit,
8489            superuser,
8490            is_user,
8491        }))
8492    }
8493
8494    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8495    /// consumed the USING / WITH CHECK keyword.
8496    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8497        if !matches!(self.peek(), Token::LParen) {
8498            return Err(self.err(alloc::format!(
8499                "expected '(' after {clause}, got {:?}",
8500                self.peek()
8501            )));
8502        }
8503        self.advance();
8504        let e = self.parse_expr(0)?;
8505        if !matches!(self.peek(), Token::RParen) {
8506            return Err(self.err(alloc::format!(
8507                "expected ')' to close {clause}, got {:?}",
8508                self.peek()
8509            )));
8510        }
8511        self.advance();
8512        Ok(e)
8513    }
8514
8515    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8516    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8517        let mut roles = Vec::new();
8518        loop {
8519            roles.push(self.expect_ident_like()?);
8520            if matches!(self.peek(), Token::Comma) {
8521                self.advance();
8522            } else {
8523                break;
8524            }
8525        }
8526        Ok(roles)
8527    }
8528
8529    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8530    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8531    /// `CREATE POLICY`.
8532    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8533        use crate::ast::PolicyCmd;
8534        let name = self.expect_ident_like()?;
8535        if !matches!(self.peek(), Token::On) {
8536            return Err(self.err(alloc::format!(
8537                "expected ON after CREATE POLICY name, got {:?}",
8538                self.peek()
8539            )));
8540        }
8541        self.advance();
8542        let table = self.expect_ident_like()?;
8543
8544        let mut permissive = true;
8545        if matches!(self.peek(), Token::As) {
8546            self.advance();
8547            let w = self.expect_ident_like()?;
8548            permissive = if w.eq_ignore_ascii_case("permissive") {
8549                true
8550            } else if w.eq_ignore_ascii_case("restrictive") {
8551                false
8552            } else {
8553                return Err(self.err(alloc::format!(
8554                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8555                )));
8556            };
8557        }
8558
8559        let mut cmd = PolicyCmd::All;
8560        if matches!(self.peek(), Token::For) {
8561            self.advance();
8562            cmd = self.parse_policy_cmd()?;
8563        }
8564
8565        let mut roles = Vec::new();
8566        if matches!(self.peek(), Token::To) {
8567            self.advance();
8568            roles = self.parse_policy_roles()?;
8569        }
8570
8571        let mut using = None;
8572        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8573        {
8574            self.advance();
8575            using = Some(self.parse_paren_expr("USING")?);
8576        }
8577
8578        let mut with_check = None;
8579        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8580        {
8581            self.advance();
8582            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8583            {
8584                return Err(self.err(alloc::format!(
8585                    "expected CHECK after WITH, got {:?}",
8586                    self.peek()
8587                )));
8588            }
8589            self.advance();
8590            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8591        }
8592
8593        // Clause-per-command matrix (PG wording).
8594        match cmd {
8595            PolicyCmd::Insert => {
8596                if using.is_some() {
8597                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8598                }
8599            }
8600            PolicyCmd::Select | PolicyCmd::Delete => {
8601                if with_check.is_some() {
8602                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8603                }
8604            }
8605            PolicyCmd::Update | PolicyCmd::All => {}
8606        }
8607
8608        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8609            name,
8610            table,
8611            permissive,
8612            cmd,
8613            roles,
8614            using,
8615            with_check,
8616        }))
8617    }
8618
8619    /// v7.39 (RLS) — the command word after `FOR`.
8620    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8621        use crate::ast::PolicyCmd;
8622        match self.peek().clone() {
8623            Token::All => {
8624                self.advance();
8625                Ok(PolicyCmd::All)
8626            }
8627            Token::Select => {
8628                self.advance();
8629                Ok(PolicyCmd::Select)
8630            }
8631            Token::Insert => {
8632                self.advance();
8633                Ok(PolicyCmd::Insert)
8634            }
8635            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8636                self.advance();
8637                Ok(PolicyCmd::Update)
8638            }
8639            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8640                self.advance();
8641                Ok(PolicyCmd::Delete)
8642            }
8643            other => Err(self.err(alloc::format!(
8644                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8645            ))),
8646        }
8647    }
8648
8649    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8650    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8651    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8652        let name = self.expect_ident_like()?;
8653        if !matches!(self.peek(), Token::On) {
8654            return Err(self.err(alloc::format!(
8655                "expected ON after ALTER POLICY name, got {:?}",
8656                self.peek()
8657            )));
8658        }
8659        self.advance();
8660        let table = self.expect_ident_like()?;
8661
8662        // RENAME TO new
8663        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8664        {
8665            self.advance();
8666            if !matches!(self.peek(), Token::To) {
8667                return Err(self.err(alloc::format!(
8668                    "expected TO after RENAME, got {:?}",
8669                    self.peek()
8670                )));
8671            }
8672            self.advance();
8673            let new = self.expect_ident_like()?;
8674            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8675                name,
8676                table,
8677                rename_to: Some(new),
8678                roles: None,
8679                using: None,
8680                with_check: None,
8681            }));
8682        }
8683
8684        let mut roles = None;
8685        if matches!(self.peek(), Token::To) {
8686            self.advance();
8687            roles = Some(self.parse_policy_roles()?);
8688        }
8689        let mut using = None;
8690        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8691        {
8692            self.advance();
8693            using = Some(self.parse_paren_expr("USING")?);
8694        }
8695        let mut with_check = None;
8696        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8697        {
8698            self.advance();
8699            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8700            {
8701                return Err(self.err(alloc::format!(
8702                    "expected CHECK after WITH, got {:?}",
8703                    self.peek()
8704                )));
8705            }
8706            self.advance();
8707            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8708        }
8709        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8710            name,
8711            table,
8712            rename_to: None,
8713            roles,
8714            using,
8715            with_check,
8716        }))
8717    }
8718
8719    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8720    /// `DROP POLICY`.
8721    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8722        let if_exists = self.consume_if_exists();
8723        let name = self.expect_ident_like()?;
8724        if !matches!(self.peek(), Token::On) {
8725            return Err(self.err(alloc::format!(
8726                "expected ON after DROP POLICY name, got {:?}",
8727                self.peek()
8728            )));
8729        }
8730        self.advance();
8731        let table = self.expect_ident_like()?;
8732        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8733            name,
8734            table,
8735            if_exists,
8736        }))
8737    }
8738}
8739fn wrap_from_leaves(
8740    e: &mut Expr,
8741    names: &[String],
8742    make: &dyn Fn(Expr) -> Expr,
8743    refs: &dyn Fn(&Expr) -> bool,
8744) {
8745    if let Expr::Column(c) = e {
8746        if c.qualifier
8747            .as_deref()
8748            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8749        {
8750            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8751            *e = make(taken);
8752        }
8753        return;
8754    }
8755    match e {
8756        Expr::Binary { lhs, rhs, .. } => {
8757            wrap_from_leaves(lhs, names, make, refs);
8758            wrap_from_leaves(rhs, names, make, refs);
8759        }
8760        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8761            wrap_from_leaves(expr, names, make, refs)
8762        }
8763        Expr::FunctionCall { args, .. } => {
8764            for a in args.iter_mut() {
8765                wrap_from_leaves(a, names, make, refs);
8766            }
8767        }
8768        Expr::Case {
8769            operand,
8770            branches,
8771            else_branch,
8772        } => {
8773            if let Some(o) = operand.as_deref_mut() {
8774                wrap_from_leaves(o, names, make, refs);
8775            }
8776            for (w, t) in branches.iter_mut() {
8777                wrap_from_leaves(w, names, make, refs);
8778                wrap_from_leaves(t, names, make, refs);
8779            }
8780            if let Some(el) = else_branch.as_deref_mut() {
8781                wrap_from_leaves(el, names, make, refs);
8782            }
8783        }
8784        // Compound variants the walk doesn't decompose: keep the
8785        // pre-D.30 behavior — wrap the whole sub-expr if it touches
8786        // a source table, so nothing regresses.
8787        other => {
8788            if refs(other) {
8789                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8790                *other = make(taken);
8791            }
8792        }
8793    }
8794}
8795
8796/// v7.39 (round 241) — does this expression reference any of the FROM /
8797/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8798/// lowerings)?
8799fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8800    match e {
8801        Expr::Column(c) => c
8802            .qualifier
8803            .as_deref()
8804            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8805        Expr::Binary { lhs, rhs, .. } => {
8806            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8807        }
8808        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8809        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8810        Expr::Case {
8811            operand,
8812            branches,
8813            else_branch,
8814        } => {
8815            operand
8816                .as_deref()
8817                .is_some_and(|o| expr_refs_tables(o, names))
8818                || branches
8819                    .iter()
8820                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8821                || else_branch
8822                    .as_deref()
8823                    .is_some_and(|el| expr_refs_tables(el, names))
8824        }
8825        _ => false,
8826    }
8827}
8828
8829impl Parser {
8830    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8831    /// Caller already consumed the leading `UPDATE` ident.
8832    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8833    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8834    /// after the target name has been read. `JOIN` is a reserved token;
8835    /// the qualifiers are bare idents.
8836    fn peek_is_update_join_start(&self) -> bool {
8837        match self.peek() {
8838            // JOIN and its qualifiers are reserved lexer tokens (the grammar
8839            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8840            Token::Join
8841            | Token::Inner
8842            | Token::Left
8843            | Token::Right
8844            | Token::Cross
8845            | Token::Full => true,
8846            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8847            Token::Ident(s) | Token::QuotedIdent(s) => {
8848                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8849            }
8850            _ => false,
8851        }
8852    }
8853
8854    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8855    /// USER-variable assignment. Its own per-session namespace, an arbitrary
8856    /// expression on the right, and `:=` as a second spelling of `=`.
8857    ///
8858    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8859    /// from sits on the nesting recursion chain (a CTE body, a subquery),
8860    /// and holding this loop's `Vec` + `String` locals there overflowed the
8861    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8862    #[inline(never)]
8863    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8864        let mut assigns: Vec<(String, Expr)> = Vec::new();
8865        let mut settings: Vec<(String, Expr)> = Vec::new();
8866        loop {
8867            // v7.39 (round 554) — a plain NAME here is a session
8868            // setting, not a user variable. mysqldump writes the two in
8869            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8870            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8871            // changes it — and this refused the mixture outright, so no
8872            // dump could be restored past its preamble.
8873            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
8874                self.advance();
8875                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8876                    return Err(self.err(alloc::format!(
8877                        "expected `=` after {name}, got {:?}",
8878                        self.peek()
8879                    )));
8880                }
8881                self.advance();
8882                let value = self.parse_expr(0)?;
8883                settings.push((name.to_ascii_lowercase(), value));
8884                if matches!(self.peek(), Token::Comma) {
8885                    self.advance();
8886                    continue;
8887                }
8888                break;
8889            }
8890            let Token::SessionVar(raw) = self.peek().clone() else {
8891                return Err(self.err(alloc::format!(
8892                    "expected a user variable after SET, got {:?}",
8893                    self.peek()
8894                )));
8895            };
8896            if raw.starts_with("@@") {
8897                return Err(self.err(alloc::string::String::from(
8898                    "cannot mix `@@` settings with `@` user variables in one SET",
8899                )));
8900            }
8901            self.advance();
8902            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8903                return Err(self.err(alloc::format!(
8904                    "expected `=` or `:=` after {raw}, got {:?}",
8905                    self.peek()
8906                )));
8907            }
8908            self.advance();
8909            let value = self.parse_expr(0)?;
8910            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
8911            if matches!(self.peek(), Token::Comma) {
8912                self.advance();
8913                continue;
8914            }
8915            break;
8916        }
8917        Ok(Statement::SetUserVars(assigns, settings))
8918    }
8919
8920    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
8921        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
8922        // NAMED `only` until now, which failed on `relation "only" does
8923        // not exist`. The lookahead is what keeps a table actually
8924        // called `only` working: the keyword is only a keyword when a
8925        // TABLE NAME follows it — and `SET` arrives as an identifier
8926        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
8927        // for the table and die on the `=`. Measured by the pin.
8928        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
8929            if s.eq_ignore_ascii_case("only"))
8930            && matches!(
8931                self.tokens.get(self.pos + 1),
8932                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
8933            );
8934        if only {
8935            self.advance();
8936        }
8937        let table = self.expect_ident_like()?;
8938        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
8939        // bare spelling; a bare identifier that is the SET keyword itself
8940        // is the clause, not an alias.
8941        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
8942        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
8943        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
8944        // following JOIN a syntax error.
8945        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
8946        let alias = if matches!(self.peek(), Token::As) {
8947            self.advance();
8948            Some(self.expect_ident_like()?)
8949        } else {
8950            match self.peek() {
8951                Token::Ident(s) | Token::QuotedIdent(s)
8952                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
8953                {
8954                    let a = s.clone();
8955                    self.advance();
8956                    Some(a)
8957                }
8958                _ => None,
8959            }
8960        };
8961        // v7.39 (round 420) — MySQL's multi-table UPDATE:
8962        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
8963        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
8964        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
8965        // The FIRST table is the mutation target and the rest are sources —
8966        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
8967        // SPG already lowers onto correlated subqueries. So rewind, let
8968        // `parse_from_clause` read the whole list (it handles aliases, comma
8969        // lists, and every JOIN form), then peel the target off the front.
8970        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
8971            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
8972        {
8973            // NOTE: `advance()` destroys the tokens it returns
8974            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
8975            // is NOT possible — the tail is read forward, once, through the
8976            // same grammar `parse_from_clause` uses after its primary.
8977            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
8978            let mut joins = self.parse_from_joins(&target_qual)?;
8979            if joins.is_empty() {
8980                return Err(self.err(alloc::string::String::from(
8981                    "multi-table UPDATE needs at least one source table",
8982                )));
8983            }
8984            let head = joins.remove(0);
8985            // A LEFT join keeps every target row (the unmatched ones see NULL
8986            // on the source side), so it must NOT get the EXISTS row filter
8987            // the inner / comma forms use.
8988            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
8989            let src = FromClause {
8990                primary: head.table,
8991                joins,
8992            };
8993            (Some(src), head.on, outer)
8994        } else {
8995            (None, None, false)
8996        };
8997        self.expect_keyword_ident("set")?;
8998        let mut assignments = Vec::new();
8999        loop {
9000            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9001            // …)` — the parenthesized multi-assignment. Expressions
9002            // assign positionally; a subquery RHS clones per column
9003            // keeping only the Nth projection item.
9004            if matches!(self.peek(), Token::LParen) {
9005                self.advance();
9006                let mut cols = alloc::vec![self.expect_ident_like()?];
9007                while matches!(self.peek(), Token::Comma) {
9008                    self.advance();
9009                    cols.push(self.expect_ident_like()?);
9010                }
9011                if !matches!(self.peek(), Token::RParen) {
9012                    return Err(self.err(format!(
9013                        "expected ')' after SET column list, got {:?}",
9014                        self.peek()
9015                    )));
9016                }
9017                self.advance();
9018                if !matches!(self.peek(), Token::Eq) {
9019                    return Err(self.err(format!(
9020                        "expected `=` after SET column list, got {:?}",
9021                        self.peek()
9022                    )));
9023                }
9024                self.advance();
9025                if !matches!(self.peek(), Token::LParen) {
9026                    return Err(self.err(format!(
9027                        "expected '(' after SET (…) =, got {:?}",
9028                        self.peek()
9029                    )));
9030                }
9031                self.advance();
9032                if matches!(self.peek(), Token::Select) {
9033                    let inner = match self.parse_select_stmt()? {
9034                        Statement::Select(s) => s,
9035                        other => {
9036                            return Err(self.err(alloc::format!(
9037                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9038                            )));
9039                        }
9040                    };
9041                    if !matches!(self.peek(), Token::RParen) {
9042                        return Err(self.err(format!(
9043                            "expected ')' after SET subquery, got {:?}",
9044                            self.peek()
9045                        )));
9046                    }
9047                    self.advance();
9048                    if inner.items.len() != cols.len() {
9049                        return Err(self.err(alloc::format!(
9050                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9051                            cols.len(),
9052                            inner.items.len()
9053                        )));
9054                    }
9055                    for (i, col) in cols.into_iter().enumerate() {
9056                        let mut sub = inner.clone();
9057                        sub.items = alloc::vec![sub.items[i].clone()];
9058                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9059                    }
9060                } else {
9061                    let mut exprs = alloc::vec![self.parse_expr(0)?];
9062                    while matches!(self.peek(), Token::Comma) {
9063                        self.advance();
9064                        exprs.push(self.parse_expr(0)?);
9065                    }
9066                    if !matches!(self.peek(), Token::RParen) {
9067                        return Err(self.err(format!(
9068                            "expected ')' after SET row values, got {:?}",
9069                            self.peek()
9070                        )));
9071                    }
9072                    self.advance();
9073                    if exprs.len() != cols.len() {
9074                        return Err(self.err(alloc::format!(
9075                            "SET (…) = (…) arity mismatch: {} columns, {} values",
9076                            cols.len(),
9077                            exprs.len()
9078                        )));
9079                    }
9080                    for (col, e) in cols.into_iter().zip(exprs) {
9081                        assignments.push((col, e));
9082                    }
9083                }
9084                if matches!(self.peek(), Token::Comma) {
9085                    self.advance();
9086                    continue;
9087                }
9088                break;
9089            }
9090            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9091            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9092            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9093            // `public.` dump qualifiers), so the qualifier has to be read off
9094            // the token stream first — otherwise `SET b.v = 888` would write
9095            // to the TARGET table's `v` while naming a source table, a
9096            // silent-wrong. A qualifier naming a SOURCE table means a
9097            // multi-TARGET update — mutating two tables in one statement —
9098            // which SPG does not model, so it is refused loudly.
9099            let set_qual: Option<String> = if mysql_from.is_some()
9100                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9101            {
9102                match self.peek() {
9103                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9104                    _ => None,
9105                }
9106            } else {
9107                None
9108            };
9109            let col = self.expect_ident_like()?;
9110            if let Some(q) = set_qual {
9111                let names_target = q.eq_ignore_ascii_case(&table)
9112                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9113                if !names_target {
9114                    return Err(self.err(alloc::format!(
9115                        "multi-table UPDATE can only assign to its first table \
9116                         ({table}); `{q}.{col}` targets another table"
9117                    )));
9118                }
9119            }
9120            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9121            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9122            // `__column_default` marker lowering just below). PG assigns to the
9123            // i-th (1-based) element, NULL-padding when i exceeds the length.
9124            if matches!(self.peek(), Token::LBracket) {
9125                self.advance();
9126                let index = self.parse_expr(0)?;
9127                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9128                // (and the open `arr[lo:]`), lowered to
9129                // `__array_assign_slice`. Only the single-subscript form
9130                // parsed before, so a slice assignment was a syntax error.
9131                let mut slice_hi: Option<Option<Expr>> = None;
9132                if matches!(self.peek(), Token::Colon) {
9133                    self.advance();
9134                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9135                        None
9136                    } else {
9137                        Some(self.parse_expr(0)?)
9138                    });
9139                }
9140                if !matches!(self.peek(), Token::RBracket) {
9141                    return Err(self.err(format!(
9142                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9143                        self.peek()
9144                    )));
9145                }
9146                self.advance();
9147                if !matches!(self.peek(), Token::Eq) {
9148                    return Err(self.err(format!(
9149                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9150                        self.peek()
9151                    )));
9152                }
9153                self.advance();
9154                let value = self.parse_expr(0)?;
9155                // PG merges several subscript writes to the same column into one
9156                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9157                // assignment to `col` rather than each overwriting the original.
9158                let existing = assignments.iter().position(|(c, _)| c == &col);
9159                let base = match existing {
9160                    Some(i) => assignments[i].1.clone(),
9161                    None => Expr::Column(ColumnName {
9162                        qualifier: None,
9163                        name: col.clone(),
9164                    }),
9165                };
9166                let assigned = match slice_hi {
9167                    None => Expr::FunctionCall {
9168                        name: "__array_assign".to_string(),
9169                        args: alloc::vec![base, index, value],
9170                    },
9171                    Some(hi) => Expr::FunctionCall {
9172                        name: "__array_assign_slice".to_string(),
9173                        args: alloc::vec![
9174                            base,
9175                            index,
9176                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9177                            value,
9178                        ],
9179                    },
9180                };
9181                match existing {
9182                    Some(i) => assignments[i].1 = assigned,
9183                    None => assignments.push((col, assigned)),
9184                }
9185                if matches!(self.peek(), Token::Comma) {
9186                    self.advance();
9187                    continue;
9188                }
9189                break;
9190            }
9191            if !matches!(self.peek(), Token::Eq) {
9192                return Err(self.err(format!(
9193                    "expected `=` after column name in UPDATE SET, got {:?}",
9194                    self.peek()
9195                )));
9196            }
9197            self.advance();
9198            // `SET col = DEFAULT` — the column's declared default;
9199            // rides out as a marker call the update executor
9200            // resolves against the schema.
9201            let value = if matches!(self.peek(), Token::Default) {
9202                self.advance();
9203                Expr::FunctionCall {
9204                    name: "__column_default".to_string(),
9205                    args: Vec::new(),
9206                }
9207            } else {
9208                self.parse_expr(0)?
9209            };
9210            assignments.push((col, value));
9211            if matches!(self.peek(), Token::Comma) {
9212                self.advance();
9213                continue;
9214            }
9215            break;
9216        }
9217        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9218        // update. Lowers onto the correlated-subquery machinery:
9219        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9220        // and each assignment that references a FROM-list table
9221        // wraps into a correlated scalar subquery
9222        // (SELECT expr FROM src WHERE cond). Equivalent for the
9223        // unique-join shape (the overwhelmingly common one); a
9224        // multi-match, which PG resolves by arbitrary pick,
9225        // surfaces as a scalar-subquery cardinality error instead
9226        // of a silent arbitrary result.
9227        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9228        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9229        // the SAME lowering below. Both spellings together is not legal in
9230        // either dialect.
9231        let from_clause = if let Some(fc) = mysql_from {
9232            if matches!(self.peek(), Token::From) {
9233                return Err(self.err(alloc::string::String::from(
9234                    "multi-table UPDATE already names its sources; drop the FROM clause",
9235                )));
9236            }
9237            Some(fc)
9238        } else if matches!(self.peek(), Token::From) {
9239            self.advance();
9240            Some(self.parse_from_clause()?)
9241        } else {
9242            None
9243        };
9244        let where_ = if matches!(self.peek(), Token::Where) {
9245            self.advance();
9246            Some(self.parse_expr(0)?)
9247        } else {
9248            None
9249        };
9250        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9251        // and the TARGET-row filter are NOT the same predicate once a LEFT
9252        // join is involved:
9253        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9254        //     one conjunction, and the whole thing filters target rows via
9255        //     EXISTS.
9256        //   * LEFT join: only the ON predicate belongs inside the source
9257        //     subquery. The WHERE still filters TARGET rows (with source
9258        //     columns read through the correlated subquery, which yields NULL
9259        //     for an unmatched row — exactly LEFT-join semantics).
9260        // Round 420 folded ON into WHERE unconditionally and then dropped the
9261        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9262        // WHERE a.id > 1` updated EVERY row.
9263        let sub_where = match (mysql_on.clone(), where_.clone()) {
9264            _ if mysql_outer => mysql_on.clone(),
9265            (Some(on), Some(w)) => Some(Expr::Binary {
9266                lhs: Box::new(on),
9267                op: crate::ast::BinOp::And,
9268                rhs: Box::new(w),
9269            }),
9270            (Some(on), None) => Some(on),
9271            (None, w) => w,
9272        };
9273        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9274        // has no such clause on UPDATE, so this is accepted only under the
9275        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9276        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9277        let mut returning = self.parse_optional_returning()?;
9278        // v7.39 (round 533) — kept for the engine, which can resolve the
9279        // UNQUALIFIED leaves this lowering has to leave alone.
9280        let from_sources = from_clause.as_ref().map(|fc| {
9281            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9282                from: fc.clone(),
9283                sub_where: sub_where.clone(),
9284            })
9285        });
9286        let (assignments, where_) = if let Some(fc) = from_clause {
9287            let names: Vec<String> = core::iter::once(&fc.primary)
9288                .chain(fc.joins.iter().map(|j| &j.table))
9289                .flat_map(|t| {
9290                    t.alias
9291                        .clone()
9292                        .into_iter()
9293                        .chain(core::iter::once(t.name.clone()))
9294                })
9295                .collect();
9296            let refs_list = |e: &Expr| -> bool {
9297                fn walk(e: &Expr, names: &[String]) -> bool {
9298                    match e {
9299                        Expr::Column(c) => c
9300                            .qualifier
9301                            .as_deref()
9302                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9303                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9304                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9305                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9306                        Expr::Case {
9307                            operand,
9308                            branches,
9309                            else_branch,
9310                        } => {
9311                            operand.as_deref().is_some_and(|o| walk(o, names))
9312                                || branches
9313                                    .iter()
9314                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9315                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9316                        }
9317                        _ => false,
9318                    }
9319                }
9320                walk(e, &names)
9321            };
9322            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9323                locking: None,
9324                ctes: Vec::new(),
9325                distinct: false,
9326                distinct_on: Vec::new(),
9327                items,
9328                from: Some(fc.clone()),
9329                where_: sub_where.clone(),
9330                group_by: None,
9331                group_by_all: false,
9332                having: None,
9333                unions: Vec::new(),
9334                order_by: Vec::new(),
9335                limit: None,
9336                offset: None,
9337                limit_with_ties: false,
9338                window_check_exprs: Vec::new(),
9339            };
9340            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9341            // assignment RHS with a correlated scalar subquery, instead of
9342            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9343            // column reference (`SET v = v + u.bonus`, where `v` is the target
9344            // table's column) inside a subquery whose FROM only has the source
9345            // table, so the unqualified `v` resolved against the source and
9346            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9347            // context — where they belong — fixes it; only the source columns
9348            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9349            // compound variants the leaf-walk doesn't decompose.
9350            let make_subq = |inner: Expr| {
9351                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9352                    expr: inner,
9353                    alias: None,
9354                }])))
9355            };
9356            let assignments = assignments
9357                .into_iter()
9358                .map(|(col, mut expr)| {
9359                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9360                    (col, expr)
9361                })
9362                .collect();
9363            let exists = Expr::Exists {
9364                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9365                    expr: Expr::Literal(Literal::Integer(1)),
9366                    alias: None,
9367                }])),
9368                negated: false,
9369            };
9370            // v7.39 (round 241) — RETURNING may reference the FROM-list
9371            // tables too (`RETURNING emp.id, dept.name`); the same
9372            // leaf-to-correlated-subquery lowering the assignments get.
9373            // Without it the qualifier died at eval with "unknown table
9374            // qualifier". (RETURNING was parsed before this block — the
9375            // lowering is a pure AST transformation.)
9376            if let Some(items) = returning.as_mut() {
9377                for item in items.iter_mut() {
9378                    if let SelectItem::Expr { expr, .. } = item {
9379                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9380                    }
9381                }
9382            }
9383            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9384            // EVERY matching target row: it gets no EXISTS filter, but the
9385            // caller's WHERE still applies, with source columns read through
9386            // the correlated subquery (NULL when unmatched — LEFT-join
9387            // semantics). `sub_where` above already excluded the WHERE from
9388            // the source subquery for this case.
9389            if mysql_outer {
9390                let mut outer = where_;
9391                if let Some(w) = outer.as_mut() {
9392                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9393                }
9394                (assignments, outer)
9395            } else {
9396                (assignments, Some(exists))
9397            }
9398        } else {
9399            (assignments, where_)
9400        };
9401        Ok(Statement::Update(crate::ast::UpdateStatement {
9402            ctes: Vec::new(),
9403            table,
9404            only,
9405            alias,
9406            assignments,
9407            from_sources,
9408            where_,
9409            order_limit: update_order_limit,
9410            returning,
9411        }))
9412    }
9413
9414    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9415    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9416    /// clause and its meaning are identical, so both call this rather than
9417    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9418    /// legal. PG has no such clause on either statement, so it is read only
9419    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9420    /// errors.
9421    ///
9422    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9423    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9424    /// stack in round 430.
9425    #[inline(never)]
9426    fn parse_mysql_dml_order_limit(
9427        &mut self,
9428        what: &str,
9429    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9430        if !self.mysql_dialect {
9431            return Ok(None);
9432        }
9433        let order_by = self.parse_order_by_keys()?;
9434        let limit = if matches!(self.peek(), Token::Limit) {
9435            self.advance();
9436            let tok = self.advance();
9437            let Token::Integer(n) = tok else {
9438                return Err(self.err(alloc::format!(
9439                    "expected integer after {what} LIMIT, got {tok:?}"
9440                )));
9441            };
9442            // MySQL rejects the `LIMIT offset, count` form here — only a
9443            // single row count is legal on a DML statement.
9444            if matches!(self.peek(), Token::Comma) {
9445                return Err(self.err(alloc::format!(
9446                    "{what} LIMIT takes a row count, not an offset"
9447                )));
9448            }
9449            let n = u32::try_from(n)
9450                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9451            Some(n)
9452        } else {
9453            None
9454        };
9455        if order_by.is_empty() && limit.is_none() {
9456            return Ok(None);
9457        }
9458        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9459            order_by,
9460            limit,
9461        })))
9462    }
9463
9464    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9465    /// the leading `DELETE` ident.
9466    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9467        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9468        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9469        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9470        // parse here; it reaches the existing USING path with the target
9471        // repeated in the list, which the source-list peel below handles.)
9472        // More than one name is a multi-TARGET delete, which SPG does not
9473        // model; it is refused rather than half-applied.
9474        let mysql_pre_target: Option<String> =
9475            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9476                let first = self.expect_ident_like()?;
9477                if matches!(self.peek(), Token::Comma) {
9478                    return Err(self.err(alloc::format!(
9479                        "multi-table DELETE can only delete from one table; \
9480                     `DELETE {first}, …` names several"
9481                    )));
9482                }
9483                Some(first)
9484            } else {
9485                None
9486            };
9487        if !matches!(self.peek(), Token::From) {
9488            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9489        }
9490        self.advance();
9491        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9492        // lookahead as the UPDATE spelling.
9493        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9494            if s.eq_ignore_ascii_case("only"))
9495            && matches!(
9496                self.tokens.get(self.pos + 1),
9497                Some(Token::Ident(_) | Token::QuotedIdent(_))
9498            );
9499        if only {
9500            self.advance();
9501        }
9502        let table = self.expect_ident_like()?;
9503        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9504        // spelling must not swallow the clause keywords that can follow
9505        // the target.
9506        let alias = if matches!(self.peek(), Token::As) {
9507            self.advance();
9508            Some(self.expect_ident_like()?)
9509        } else {
9510            match self.peek() {
9511                Token::Ident(s) | Token::QuotedIdent(s)
9512                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9513                {
9514                    let a = s.clone();
9515                    self.advance();
9516                    Some(a)
9517                }
9518                _ => None,
9519            }
9520        };
9521        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9522        // through the SAME join grammar the FROM clause uses (see the
9523        // `advance()`-destroys-tokens note on `parse_from_joins`).
9524        let mut mysql_on: Option<Expr> = None;
9525        let mut mysql_outer = false;
9526        let mysql_using = if mysql_pre_target.is_some()
9527            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9528        {
9529            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9530            let mut joins = self.parse_from_joins(&target_qual)?;
9531            if joins.is_empty() {
9532                return Err(self.err(alloc::string::String::from(
9533                    "multi-table DELETE needs at least one source table",
9534                )));
9535            }
9536            let head = joins.remove(0);
9537            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9538            mysql_on = head.on;
9539            Some(FromClause {
9540                primary: head.table,
9541                joins,
9542            })
9543        } else {
9544            None
9545        };
9546        // The pre-FROM target must be the table the FROM names (or its
9547        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9548        // is not the scan target.
9549        if let Some(t) = &mysql_pre_target {
9550            let names_target = t.eq_ignore_ascii_case(&table)
9551                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9552            if !names_target {
9553                return Err(self.err(alloc::format!(
9554                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9555                )));
9556            }
9557        }
9558        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9559        // delete. Same lowering as UPDATE … FROM: the WHERE
9560        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9561        // target row by the correlated machinery.
9562        let using_clause = if let Some(fc) = mysql_using {
9563            Some(fc)
9564        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9565            self.advance();
9566            let mut fc = self.parse_from_clause()?;
9567            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9568            // repeats the TARGET as the first USING entry (PG's spelling
9569            // lists only the extra sources). Peel it so the source subquery
9570            // does not re-scan — and shadow — the target table.
9571            let primary_is_target =
9572                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9573            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9574                let head = fc.joins.remove(0);
9575                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9576                mysql_on = head.on;
9577                fc = FromClause {
9578                    primary: head.table,
9579                    joins: fc.joins,
9580                };
9581            }
9582            Some(fc)
9583        } else {
9584            None
9585        };
9586        let where_ = if matches!(self.peek(), Token::Where) {
9587            self.advance();
9588            Some(self.parse_expr(0)?)
9589        } else {
9590            None
9591        };
9592        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9593        // read before RETURNING (MariaDB's own extension trails the LIMIT).
9594        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9595        let mut returning = self.parse_optional_returning()?;
9596        let where_ = if let Some(fc) = using_clause {
9597            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9598            // a USING-table reference in RETURNING becomes a correlated
9599            // scalar subquery over the USING list.
9600            let names: Vec<String> = core::iter::once(&fc.primary)
9601                .chain(fc.joins.iter().map(|j| &j.table))
9602                .flat_map(|t| {
9603                    t.alias
9604                        .clone()
9605                        .into_iter()
9606                        .chain(core::iter::once(t.name.clone()))
9607                })
9608                .collect();
9609            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9610            // join filters the SOURCE subquery on the ON predicate alone and
9611            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9612            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9613            // rows); every other form folds ON and WHERE into one EXISTS.
9614            let sub_where = match (mysql_on.clone(), where_.clone()) {
9615                _ if mysql_outer => mysql_on.clone(),
9616                (Some(on), Some(w)) => Some(Expr::Binary {
9617                    lhs: Box::new(on),
9618                    op: crate::ast::BinOp::And,
9619                    rhs: Box::new(w),
9620                }),
9621                (Some(on), None) => Some(on),
9622                (None, w) => w,
9623            };
9624            let exists_where = sub_where.clone();
9625            let sub_fc = fc.clone();
9626            let make_subq = move |leaf: Expr| -> Expr {
9627                Expr::ScalarSubquery(Box::new(SelectStatement {
9628                    locking: None,
9629                    ctes: Vec::new(),
9630                    distinct: false,
9631                    distinct_on: Vec::new(),
9632                    items: alloc::vec![SelectItem::Expr {
9633                        expr: leaf,
9634                        alias: None,
9635                    }],
9636                    from: Some(sub_fc.clone()),
9637                    where_: sub_where.clone(),
9638                    group_by: None,
9639                    group_by_all: false,
9640                    having: None,
9641                    unions: Vec::new(),
9642                    order_by: Vec::new(),
9643                    limit: None,
9644                    offset: None,
9645                    limit_with_ties: false,
9646                    window_check_exprs: Vec::new(),
9647                }))
9648            };
9649            let refs = |e: &Expr| expr_refs_tables(e, &names);
9650            if let Some(items) = returning.as_mut() {
9651                for item in items.iter_mut() {
9652                    if let SelectItem::Expr { expr, .. } = item {
9653                        wrap_from_leaves(expr, &names, &make_subq, &refs);
9654                    }
9655                }
9656            }
9657            // A LEFT join deletes the target rows the WHERE selects, reading
9658            // source columns through the correlated subquery (NULL when
9659            // unmatched); no EXISTS row filter.
9660            if mysql_outer {
9661                let mut outer = where_;
9662                if let Some(w) = outer.as_mut() {
9663                    wrap_from_leaves(w, &names, &make_subq, &refs);
9664                }
9665                outer
9666            } else {
9667                Some(Expr::Exists {
9668                    subquery: Box::new(SelectStatement {
9669                        locking: None,
9670                        ctes: Vec::new(),
9671                        distinct: false,
9672                        distinct_on: Vec::new(),
9673                        items: alloc::vec![SelectItem::Expr {
9674                            expr: Expr::Literal(Literal::Integer(1)),
9675                            alias: None,
9676                        }],
9677                        from: Some(fc),
9678                        where_: exists_where,
9679                        group_by: None,
9680                        group_by_all: false,
9681                        having: None,
9682                        unions: Vec::new(),
9683                        order_by: Vec::new(),
9684                        limit: None,
9685                        offset: None,
9686                        limit_with_ties: false,
9687                        window_check_exprs: Vec::new(),
9688                    }),
9689                    negated: false,
9690                })
9691            }
9692        } else {
9693            where_
9694        };
9695        Ok(Statement::Delete(crate::ast::DeleteStatement {
9696            ctes: Vec::new(),
9697            table,
9698            only,
9699            alias,
9700            where_,
9701            order_limit: delete_order_limit,
9702            returning,
9703        }))
9704    }
9705
9706    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9707    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9708    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9709    /// keyword. v7.17 surface:
9710    ///   * source: table reference (subquery source is a follow-up)
9711    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9712    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9713    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
9714    ///     order
9715    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9716        // INTO
9717        let is_into_kw = matches!(self.peek(), Token::Into)
9718            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9719        if !is_into_kw {
9720            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9721        }
9722        self.advance();
9723        let target = self.expect_ident_like()?;
9724        // Optional alias — bare ident before USING.
9725        let target_alias = match self.peek() {
9726            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9727                Some(self.expect_ident_like()?)
9728            }
9729            _ => None,
9730        };
9731        // USING
9732        let is_using_kw = matches!(
9733            self.peek(),
9734            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9735        );
9736        if !is_using_kw {
9737            return Err(self.err(format!(
9738                "expected USING after MERGE INTO target, got {:?}",
9739                self.peek()
9740            )));
9741        }
9742        self.advance();
9743        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9744        // <table> [alias]`. PG requires an alias after a subquery source.
9745        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9746            self.advance(); // (
9747            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9748            // constant-SELECT lowering the derived-table parser uses
9749            // (PG deletes through this form; it was a parse error).
9750            let inner = if matches!(self.peek(), Token::Values) {
9751                self.advance(); // VALUES
9752                Statement::Select(self.parse_values_rows_body()?)
9753            } else {
9754                self.parse_select_stmt()?
9755            };
9756            match self.advance() {
9757                Token::RParen => {}
9758                other => {
9759                    return Err(self.err(format!(
9760                        "expected ')' after MERGE USING subquery, got {other:?}"
9761                    )));
9762                }
9763            }
9764            let Statement::Select(sub) = inner else {
9765                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9766            };
9767            (String::new(), Some(Box::new(sub)))
9768        } else {
9769            (self.expect_ident_like()?, None)
9770        };
9771        let source_alias = match self.peek() {
9772            Token::Ident(s) | Token::QuotedIdent(s)
9773                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9774            {
9775                Some(self.expect_ident_like()?)
9776            }
9777            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9778                self.advance(); // AS
9779                Some(self.expect_ident_like()?)
9780            }
9781            _ => None,
9782        };
9783        // v7.39 (round 768, F31-D5) — optional positional column-alias
9784        // list after the source alias (`s(id, v)`).
9785        let mut source_column_aliases: Vec<String> = Vec::new();
9786        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9787            self.advance();
9788            loop {
9789                source_column_aliases.push(self.expect_ident_like()?);
9790                match self.peek() {
9791                    Token::Comma => {
9792                        self.advance();
9793                    }
9794                    Token::RParen => {
9795                        self.advance();
9796                        break;
9797                    }
9798                    other => {
9799                        return Err(self.err(format!(
9800                            "expected ',' or ')' in MERGE source column list, got {other:?}"
9801                        )));
9802                    }
9803                }
9804            }
9805        }
9806        if source_select.is_some() && source_alias.is_none() {
9807            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9808        }
9809        // ON
9810        if !matches!(self.peek(), Token::On) {
9811            return Err(self.err(format!(
9812                "expected ON after MERGE … USING source, got {:?}",
9813                self.peek()
9814            )));
9815        }
9816        self.advance();
9817        let on = self.parse_expr(0)?;
9818        // One or more WHEN clauses.
9819        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9820        loop {
9821            let is_when_kw = matches!(
9822                self.peek(),
9823                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9824            );
9825            if !is_when_kw {
9826                break;
9827            }
9828            self.advance(); // WHEN
9829            // [NOT] MATCHED
9830            let matched = if matches!(self.peek(), Token::Not) {
9831                self.advance();
9832                crate::ast::MergeMatched::NotMatched
9833            } else {
9834                crate::ast::MergeMatched::Matched
9835            };
9836            let is_matched_kw = matches!(
9837                self.peek(),
9838                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9839            );
9840            if !is_matched_kw {
9841                return Err(self.err(format!(
9842                    "expected MATCHED in WHEN clause, got {:?}",
9843                    self.peek()
9844                )));
9845            }
9846            self.advance();
9847            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9848            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9849            // to fire for target rows no source row matches.
9850            let mut matched = matched;
9851            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9852                self.advance();
9853                match self.peek() {
9854                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9855                        self.advance();
9856                        matched = crate::ast::MergeMatched::NotMatchedBySource;
9857                    }
9858                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9859                        self.advance();
9860                    }
9861                    other => {
9862                        return Err(self.err(format!(
9863                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9864                        )));
9865                    }
9866                }
9867            }
9868            // Optional AND <expr>
9869            let condition = if matches!(self.peek(), Token::And) {
9870                self.advance();
9871                Some(self.parse_expr(0)?)
9872            } else {
9873                None
9874            };
9875            // THEN
9876            let is_then_kw = matches!(
9877                self.peek(),
9878                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
9879            );
9880            if !is_then_kw {
9881                return Err(self.err(format!(
9882                    "expected THEN in WHEN clause, got {:?}",
9883                    self.peek()
9884                )));
9885            }
9886            self.advance();
9887            // Action: INSERT / UPDATE / DELETE / DO NOTHING
9888            let action = match self.peek().clone() {
9889                Token::Insert => {
9890                    self.advance();
9891                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
9892                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
9893                    // VALUES (…)` omits it and fills every column in declaration
9894                    // order. PG accepts this; SPG used to require the list.
9895                    let mut columns: Vec<String> = Vec::new();
9896                    if matches!(self.peek(), Token::LParen) {
9897                        self.advance();
9898                        loop {
9899                            columns.push(self.expect_ident_like()?);
9900                            if matches!(self.peek(), Token::Comma) {
9901                                self.advance();
9902                                continue;
9903                            }
9904                            break;
9905                        }
9906                        if !matches!(self.peek(), Token::RParen) {
9907                            return Err(self.err(format!(
9908                                "expected ')' after INSERT column list, got {:?}",
9909                                self.peek()
9910                            )));
9911                        }
9912                        self.advance();
9913                    }
9914                    // VALUES (...)
9915                    if !matches!(self.peek(), Token::Values) {
9916                        return Err(self.err(format!(
9917                            "expected VALUES in MERGE INSERT, got {:?}",
9918                            self.peek()
9919                        )));
9920                    }
9921                    self.advance();
9922                    if !matches!(self.peek(), Token::LParen) {
9923                        return Err(self.err(format!(
9924                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
9925                            self.peek()
9926                        )));
9927                    }
9928                    self.advance();
9929                    let mut values: Vec<crate::ast::Expr> = Vec::new();
9930                    loop {
9931                        values.push(self.parse_expr(0)?);
9932                        if matches!(self.peek(), Token::Comma) {
9933                            self.advance();
9934                            continue;
9935                        }
9936                        break;
9937                    }
9938                    if !matches!(self.peek(), Token::RParen) {
9939                        return Err(self.err(format!(
9940                            "expected ')' after MERGE INSERT values, got {:?}",
9941                            self.peek()
9942                        )));
9943                    }
9944                    self.advance();
9945                    // Empty column list = positional into every column, so the
9946                    // count is checked against the table arity at execution.
9947                    if !columns.is_empty() && columns.len() != values.len() {
9948                        return Err(self.err(format!(
9949                            "MERGE INSERT column count ({}) ≠ value count ({})",
9950                            columns.len(),
9951                            values.len()
9952                        )));
9953                    }
9954                    crate::ast::MergeAction::Insert { columns, values }
9955                }
9956                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
9957                    self.advance();
9958                    // SET
9959                    let is_set_kw = matches!(
9960                        self.peek(),
9961                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
9962                    );
9963                    if !is_set_kw {
9964                        return Err(self.err(format!(
9965                            "expected SET after UPDATE in MERGE, got {:?}",
9966                            self.peek()
9967                        )));
9968                    }
9969                    self.advance();
9970                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
9971                    loop {
9972                        let col = self.expect_ident_like()?;
9973                        if !matches!(self.peek(), Token::Eq) {
9974                            return Err(self.err(format!(
9975                                "expected '=' in MERGE UPDATE assignment, got {:?}",
9976                                self.peek()
9977                            )));
9978                        }
9979                        self.advance();
9980                        let expr = self.parse_expr(0)?;
9981                        assignments.push((col, expr));
9982                        if matches!(self.peek(), Token::Comma) {
9983                            self.advance();
9984                            continue;
9985                        }
9986                        break;
9987                    }
9988                    crate::ast::MergeAction::Update { assignments }
9989                }
9990                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
9991                    self.advance();
9992                    crate::ast::MergeAction::Delete
9993                }
9994                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
9995                    self.advance();
9996                    let is_nothing_kw = matches!(
9997                        self.peek(),
9998                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
9999                    );
10000                    if !is_nothing_kw {
10001                        return Err(self.err(format!(
10002                            "expected NOTHING after DO in MERGE clause, got {:?}",
10003                            self.peek()
10004                        )));
10005                    }
10006                    self.advance();
10007                    crate::ast::MergeAction::DoNothing
10008                }
10009                other => {
10010                    return Err(self.err(format!(
10011                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10012                    )));
10013                }
10014            };
10015            // PG's grammar simply has no INSERT production under BY SOURCE
10016            // (a target row already exists there) — same syntax error.
10017            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10018                && matches!(action, crate::ast::MergeAction::Insert { .. })
10019            {
10020                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10021            }
10022            clauses.push(crate::ast::MergeWhenClause {
10023                matched,
10024                condition,
10025                action,
10026            });
10027        }
10028        if clauses.is_empty() {
10029            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10030        }
10031        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10032        // unconditional (no `AND`) WHEN of the same match kind: it could
10033        // never fire. Check per match kind in clause order.
10034        let mut seen_unconditional_matched = false;
10035        let mut seen_unconditional_not_matched = false;
10036        let mut seen_unconditional_by_source = false;
10037        for c in &clauses {
10038            let seen = match c.matched {
10039                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10040                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10041                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10042            };
10043            if *seen {
10044                return Err(self.err(String::from(
10045                    "unreachable WHEN clause specified after unconditional WHEN clause",
10046                )));
10047            }
10048            if c.condition.is_none() {
10049                *seen = true;
10050            }
10051        }
10052        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10053        let returning = self.parse_optional_returning()?;
10054        Ok(Statement::Merge(crate::ast::MergeStatement {
10055            // Attached by `parse_with_cte_then_select` when the MERGE
10056            // heads a WITH clause (round 149).
10057            ctes: Vec::new(),
10058            target,
10059            target_alias,
10060            source,
10061            source_alias,
10062            source_select,
10063            source_column_aliases,
10064            on,
10065            clauses,
10066            returning,
10067        }))
10068    }
10069
10070    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10071    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10072    /// as SELECT, so `RETURNING *`, `RETURNING col`,
10073    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10074    fn parse_optional_returning(
10075        &mut self,
10076    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10077        let is_returning_kw = matches!(
10078            self.peek(),
10079            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10080        );
10081        if !is_returning_kw {
10082            return Ok(None);
10083        }
10084        self.advance();
10085        let mut items = Vec::new();
10086        loop {
10087            items.push(self.parse_select_item()?);
10088            if matches!(self.peek(), Token::Comma) {
10089                self.advance();
10090                continue;
10091            }
10092            break;
10093        }
10094        Ok(Some(items))
10095    }
10096
10097    /// v6.0.4 — parse the tail of an ALTER statement after the
10098    /// leading `ALTER` keyword has been consumed. Only one form is
10099    /// supported in v6.0.4:
10100    ///
10101    /// ```text
10102    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10103    /// ```
10104    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10105        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10106        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10107        // exclusion) is accepted by stripping the `ONLY` keyword
10108        // before the table parse.
10109        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10110        // and the long PG-dump tail are accepted as no-ops.
10111        match self.advance() {
10112            Token::Index => {}
10113            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10114            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10115            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10116            Token::Table => {
10117                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10118                    self.advance();
10119                }
10120                return self.parse_alter_table_after_keyword();
10121            }
10122            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10123                return self.parse_alter_policy_after_keyword();
10124            }
10125            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10126                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10127                    self.advance();
10128                }
10129                return self.parse_alter_table_after_keyword();
10130            }
10131            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10132            // of the silent-noop tail.
10133            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10134                return self.parse_alter_sequence_after_keyword();
10135            }
10136            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10137            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10138            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10139            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10140                // NB: the match arm consumed `TYPE` via self.advance(); the
10141                // cursor is now at the type name — do NOT advance again.
10142                let type_name = self.expect_ident_like()?;
10143                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10144                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10145                if is_add_value {
10146                    self.advance(); // ADD
10147                    self.advance(); // VALUE
10148                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10149                    // IF/EXISTS as identifiers.
10150                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10151                    {
10152                        let n1 = self.tokens.get(self.pos + 1);
10153                        let n2 = self.tokens.get(self.pos + 2);
10154                        if matches!(n1, Some(Token::Not))
10155                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10156                        {
10157                            self.advance();
10158                            self.advance();
10159                            self.advance();
10160                            true
10161                        } else {
10162                            false
10163                        }
10164                    } else {
10165                        false
10166                    };
10167                    let label = self.expect_string_literal()?;
10168                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10169                    {
10170                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10171                        self.advance();
10172                        let anchor = self.expect_string_literal()?;
10173                        Some((is_before, anchor))
10174                    } else {
10175                        None
10176                    };
10177                    return Ok(Statement::AlterTypeAddValue {
10178                        type_name,
10179                        label,
10180                        if_not_exists,
10181                        position,
10182                    });
10183                }
10184                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10185                // Used to fall into the no-op tail below: accepted, silently
10186                // ignored. `RENAME TO <newtype>` keeps falling through.
10187                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10188                    && matches!(
10189                        self.tokens.get(self.pos + 1),
10190                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10191                    )
10192                {
10193                    self.advance(); // RENAME
10194                    self.advance(); // VALUE
10195                    let old = self.expect_string_literal()?;
10196                    if matches!(self.peek(), Token::To) {
10197                        self.advance();
10198                    } else {
10199                        self.expect_keyword_ident("to")?;
10200                    }
10201                    let new = self.expect_string_literal()?;
10202                    return Ok(Statement::AlterTypeRenameValue {
10203                        type_name,
10204                        old,
10205                        new,
10206                    });
10207                }
10208                // Other ALTER TYPE forms — the ACTION stays a no-op
10209                // (pg_dump tail), but v7.39 (round 708) the NAME is
10210                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10211                // success for a type that does not exist.
10212                self.consume_until_statement_boundary();
10213                return Ok(Statement::ValidateOnly {
10214                    kind: crate::ast::ValidateOnlyKind::TypeName,
10215                    names: alloc::vec![type_name],
10216                });
10217            }
10218            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10219            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10220            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10221            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10222            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10223            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10224            // pg_dump no-op list below: every form used to report success
10225            // and change nothing, which is worse than refusing outright
10226            // (a migration dropping a constraint kept rejecting data).
10227            // NOTE: the enclosing `match self.advance()` already consumed
10228            // the DOMAIN keyword, so the name is next.
10229            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10230                return self.parse_alter_domain_after_keyword();
10231            }
10232            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10233            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10234            // used to fall into the pg_dump no-op tail below, so a DBA
10235            // setting a per-role default was told it worked and nothing
10236            // happened. Intercepted here, BEFORE that tail.
10237            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10238            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10239            // interception below exists: swallowed with the no-op tail, an
10240            // unknown parameter name was ACCEPTED where PG18 answers
10241            // `unrecognized configuration parameter`. SPG applies nothing
10242            // either way — there is no postgresql.auto.conf — but it now
10243            // says so about a name it does not know.
10244            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10245                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10246                // already consumed here. An extra advance eats the SET and
10247                // the parameter name is never seen — which is exactly the
10248                // bug a panic in this branch disproved: the branch WAS on
10249                // the path, the reading of it was wrong.
10250                let mut parameter = None;
10251                // SET <name> … | RESET <name> | RESET ALL
10252                if matches!(self.peek(), Token::Ident(k)
10253                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10254                {
10255                    self.advance();
10256                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10257                        && !n.eq_ignore_ascii_case("all")
10258                    {
10259                        self.advance();
10260                        // A dotted GUC (`plpgsql.check_asserts`) is two
10261                        // tokens; keep the whole name.
10262                        let mut full = n;
10263                        while matches!(self.peek(), Token::Dot) {
10264                            self.advance();
10265                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10266                                full.push('.');
10267                                full.push_str(&t);
10268                            }
10269                        }
10270                        parameter = Some(full);
10271                    }
10272                }
10273                self.consume_until_statement_boundary();
10274                return Ok(Statement::AlterSystem { parameter });
10275            }
10276            Token::Ident(s) | Token::QuotedIdent(s)
10277                if matches!(
10278                    s.to_ascii_lowercase().as_str(),
10279                    "role" | "user" | "database"
10280                ) && self.peeks_db_role_setting() =>
10281            {
10282                let is_database = s.eq_ignore_ascii_case("database");
10283                return self.parse_db_role_setting(is_database);
10284            }
10285            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10286            // (the non-SET forms; SET/RESET took the branch above). The
10287            // attributes still no-op — recorded, and the ignored PASSWORD
10288            // is ledgered as its own follow-up — but the ROLE is validated:
10289            // any name was accepted for a role that does not exist.
10290            Token::Ident(s) | Token::QuotedIdent(s)
10291                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10292            {
10293                // NB: the enclosing `match self.advance()` already consumed
10294                // ROLE/USER — the round-695 trap, hit again in this round's
10295                // first draft (the name was eaten and WITH parsed as the
10296                // role). The cursor is at the name.
10297                let name = self.expect_ident_or_string()?;
10298                // v7.39 (round 750) — scan the attribute tail for
10299                // PASSWORD. Everything else stays a recorded no-op, but
10300                // a dropped credential rotation is a SECURITY bug:
10301                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10302                // changed nothing, so the old password kept working.
10303                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10304                // NULL` clears the credential.
10305                let mut password: Option<Option<String>> = None;
10306                loop {
10307                    match self.peek() {
10308                        Token::Semicolon | Token::Eof => break,
10309                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10310                            self.advance();
10311                            match self.advance() {
10312                                Token::String(p) => password = Some(Some(p)),
10313                                Token::Null => password = Some(None),
10314                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10315                                    password = Some(None);
10316                                }
10317                                other => {
10318                                    return Err(self.err(alloc::format!(
10319                                        "expected password string or NULL after PASSWORD, got {other:?}"
10320                                    )));
10321                                }
10322                            }
10323                        }
10324                        _ => {
10325                            self.advance();
10326                        }
10327                    }
10328                }
10329                if name.eq_ignore_ascii_case("all") {
10330                    // `ALTER ROLE ALL …` names every role; nothing to check.
10331                    return Ok(Statement::Empty);
10332                }
10333                if let Some(pw) = password {
10334                    return Ok(Statement::AlterRolePassword { name, password: pw });
10335                }
10336                return Ok(Statement::ValidateOnly {
10337                    kind: crate::ast::ValidateOnlyKind::RoleName,
10338                    names: alloc::vec![name],
10339                });
10340            }
10341            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10342            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10343            // list far enough to validate the NAME; the actions still no-op.
10344            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10345            // models none of them and their dumps are rare.)
10346            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10347                let name = self.expect_ident_or_string()?;
10348                self.consume_until_statement_boundary();
10349                return Ok(Statement::ValidateOnly {
10350                    kind: crate::ast::ValidateOnlyKind::CollationName,
10351                    names: alloc::vec![name],
10352                });
10353            }
10354            Token::Ident(s) | Token::QuotedIdent(s)
10355                if s.eq_ignore_ascii_case("text")
10356                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10357                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10358            {
10359                self.advance(); // SEARCH
10360                self.advance(); // CONFIGURATION
10361                let name = self.expect_ident_like()?;
10362                self.consume_until_statement_boundary();
10363                return Ok(Statement::ValidateOnly {
10364                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10365                    names: alloc::vec![name],
10366                });
10367            }
10368            Token::Ident(s) | Token::QuotedIdent(s)
10369                if s.eq_ignore_ascii_case("event")
10370                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10371            {
10372                self.advance(); // TRIGGER
10373                let name = self.expect_ident_like()?;
10374                self.consume_until_statement_boundary();
10375                return Ok(Statement::ValidateOnly {
10376                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10377                    names: alloc::vec![name],
10378                });
10379            }
10380            Token::Ident(s) | Token::QuotedIdent(s)
10381                if s.eq_ignore_ascii_case("large")
10382                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10383            {
10384                self.advance(); // OBJECT
10385                let oid = match self.advance() {
10386                    Token::Integer(n) => alloc::format!("{n}"),
10387                    other => {
10388                        return Err(
10389                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10390                        );
10391                    }
10392                };
10393                self.consume_until_statement_boundary();
10394                return Ok(Statement::ValidateOnly {
10395                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10396                    names: alloc::vec![oid],
10397                });
10398            }
10399            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10400            // argument-list parse as DROP AGGREGATE (round 707); the
10401            // action no-ops, the existence check is real.
10402            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10403                // Same round-695 trap as above: AGGREGATE is already
10404                // consumed; the cursor is at the name.
10405                let name = self.expect_ident_like()?;
10406                let mut names = alloc::vec![name];
10407                if matches!(self.peek(), Token::LParen) {
10408                    self.advance();
10409                    loop {
10410                        match self.peek().clone() {
10411                            Token::RParen => {
10412                                self.advance();
10413                                break;
10414                            }
10415                            Token::Star => {
10416                                self.advance();
10417                                names.push(String::from("*"));
10418                            }
10419                            Token::Comma => {
10420                                self.advance();
10421                            }
10422                            _ => {
10423                                let mut t = self.expect_ident_like()?;
10424                                while let Token::Ident(nx) = self.peek() {
10425                                    let nx = nx.clone();
10426                                    self.advance();
10427                                    t.push(' ');
10428                                    t.push_str(&nx);
10429                                }
10430                                names.push(t);
10431                            }
10432                        }
10433                    }
10434                }
10435                self.consume_until_statement_boundary();
10436                return Ok(Statement::ValidateOnly {
10437                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10438                    names,
10439                });
10440            }
10441            Token::Ident(s) | Token::QuotedIdent(s)
10442                if matches!(
10443                    s.to_ascii_lowercase().as_str(),
10444                    "view"
10445                        | "function"
10446                        | "database"
10447                        | "schema"
10448                        | "owner"
10449                        | "default"
10450                        | "extension"
10451                        | "materialized"
10452                        | "publication"
10453                        | "subscription"
10454                        // v7.37.17 (17.6 siblings) — additional ALTER
10455                        // targets pg_dump / pg_dumpall / operator DB
10456                        // migration scripts commonly emit. SPG has
10457                        // no matching machinery for any of these; the
10458                        // parser accepts + Empty-returns so pg_dump
10459                        // tail statements don't stall.
10460                        | "tablespace"
10461                        | "language"
10462                        | "operator"
10463                        | "conversion"
10464                        | "statistics"
10465                        | "server"
10466                        | "foreign"
10467                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10468                        // / TEMPLATE (CONFIGURATION intercepted above).
10469                        | "text"
10470                ) =>
10471            {
10472                self.consume_until_statement_boundary();
10473                return Ok(Statement::Empty);
10474            }
10475            other => {
10476                return Err(self.err(format!(
10477                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10478                     after ALTER, got {other:?}"
10479                )));
10480            }
10481        }
10482        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10483        // (mailrs migrate-042 ships these). The presence of an
10484        // IF EXISTS makes the subsequent name lookup tolerate
10485        // a missing index — engine returns CommandOk no-op.
10486        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10487            let next = self.tokens.get(self.pos + 1);
10488            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10489                self.advance();
10490                self.advance();
10491                true
10492            } else {
10493                false
10494            }
10495        } else {
10496            false
10497        };
10498        let name = self.expect_ident_like()?;
10499        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10500        // Detect BEFORE the REBUILD path so the existing REBUILD
10501        // arm stays untouched.
10502        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10503            self.advance();
10504            if matches!(self.peek(), Token::To) {
10505                self.advance();
10506            } else {
10507                self.expect_keyword_ident("to")?;
10508            }
10509            let new = self.expect_ident_like()?;
10510            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10511                name,
10512                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10513            }));
10514        }
10515        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10516        // A syntax error before; the index is validated, the params no-op.
10517        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10518            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10519                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10520        {
10521            self.consume_until_statement_boundary();
10522            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10523                name,
10524                target: crate::ast::AlterIndexTarget::StorageParams,
10525            }));
10526        }
10527        // REBUILD
10528        self.expect_keyword_ident("rebuild")?;
10529        // Optional: WITH (encoding = <enc>)
10530        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10531            self.advance();
10532            if !matches!(self.peek(), Token::LParen) {
10533                return Err(self.err(format!(
10534                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10535                    self.peek()
10536                )));
10537            }
10538            self.advance();
10539            self.expect_keyword_ident("encoding")?;
10540            if !matches!(self.peek(), Token::Eq) {
10541                return Err(self.err(format!(
10542                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10543                    self.peek()
10544                )));
10545            }
10546            self.advance();
10547            let enc_ident = match self.advance() {
10548                Token::Ident(s) | Token::QuotedIdent(s) => s,
10549                other => {
10550                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10551                }
10552            };
10553            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10554                "f32" => VecEncoding::F32,
10555                "sq8" => VecEncoding::Sq8,
10556                "half" => VecEncoding::F16,
10557                other => {
10558                    return Err(self.err(format!(
10559                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10560                    )));
10561                }
10562            };
10563            if !matches!(self.peek(), Token::RParen) {
10564                return Err(self.err(format!(
10565                    "expected ')' after encoding value, got {:?}",
10566                    self.peek()
10567                )));
10568            }
10569            self.advance();
10570            Some(enc)
10571        } else {
10572            None
10573        };
10574        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10575            name,
10576            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10577        }))
10578    }
10579
10580    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10581    /// only `SET` form currently supported; future v6.7.x can add
10582    /// more SET subjects without changing the dispatch shape.
10583    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10584    /// subactions. Single-subaction shape stays a 1-element vec.
10585    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10586        let table_name = self.expect_ident_like()?;
10587        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10588        loop {
10589            let subaction = self.parse_alter_table_subaction()?;
10590            // ADD COLUMN with inline REFERENCES emits both an
10591            // AddColumn and an AddForeignKey subaction; the
10592            // helper returns 1 or 2 items.
10593            targets.extend(subaction);
10594            if matches!(self.peek(), Token::Comma) {
10595                self.advance();
10596                continue;
10597            }
10598            break;
10599        }
10600        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10601            name: table_name,
10602            targets,
10603        }))
10604    }
10605
10606    /// Parse one ALTER TABLE subaction. Returns a Vec because
10607    /// inline `REFERENCES` on `ADD COLUMN` produces both an
10608    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10609    fn parse_alter_table_subaction(
10610        &mut self,
10611    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10612        match self.peek() {
10613            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10614                self.advance();
10615                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10616                // storage parameters: paren-prefixed; consume.
10617                if matches!(self.peek(), Token::LParen) {
10618                    self.consume_until_statement_boundary();
10619                    return Ok(Vec::new());
10620                }
10621                let setting = self.expect_ident_like()?;
10622                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10623                    if !matches!(self.peek(), Token::Eq) {
10624                        return Err(self.err(alloc::format!(
10625                            "expected '=' after hot_tier_bytes, got {:?}",
10626                            self.peek()
10627                        )));
10628                    }
10629                    self.advance();
10630                    let n = self.expect_u64_literal()?;
10631                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10632                }
10633                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10634                // accept-and-no-op for ALTER TABLE SET <subject>
10635                // forms that pg_dump emits but SPG either treats
10636                // as N/A (single-tenant, single-owner, no shared
10637                // tablespaces) or accepts the dump-side declaration
10638                // without runtime effect:
10639                //   SET SCHEMA <name>            (18.11)
10640                //   SET TABLESPACE <name>        (18.8)
10641                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
10642                //   SET WITHOUT CLUSTER          (18.13)
10643                //   SET WITHOUT OIDS             (PG legacy)
10644                //   SET (option = value, …)      (storage parameters)
10645                //   SET REPLICA IDENTITY {…}     (18.14)
10646                if setting.eq_ignore_ascii_case("schema")
10647                    || setting.eq_ignore_ascii_case("tablespace")
10648                    || setting.eq_ignore_ascii_case("logged")
10649                    || setting.eq_ignore_ascii_case("unlogged")
10650                    || setting.eq_ignore_ascii_case("without")
10651                {
10652                    self.consume_until_statement_boundary();
10653                    return Ok(Vec::new());
10654                }
10655                if setting.eq_ignore_ascii_case("replica") {
10656                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10657                    self.consume_until_statement_boundary();
10658                    return Ok(Vec::new());
10659                }
10660                // SET (option=value, …) — storage parameters.
10661                if matches!(self.peek(), Token::LParen) {
10662                    self.consume_until_statement_boundary();
10663                    return Ok(Vec::new());
10664                }
10665                Err(self.err(alloc::format!(
10666                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10667                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10668                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10669                )))
10670            }
10671            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10672            // not ignored: round 645 gave SPG the inheritance the
10673            // v7.37.18 no-op said it did not have.
10674            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10675                self.advance();
10676                let parent = self.expect_ident_like()?;
10677                self.consume_until_statement_boundary();
10678                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10679                    parent,
10680                    detach: false
10681                }])
10682            }
10683            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10684            // LEVEL SECURITY`, which has its own RLS arm below — without
10685            // the guard this swallowed NO FORCE as a no-op.
10686            Token::Ident(s)
10687                if s.eq_ignore_ascii_case("no")
10688                    && !matches!(
10689                        self.tokens.get(self.pos + 1),
10690                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10691                    ) =>
10692            {
10693                self.advance();
10694                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10695                    if k.eq_ignore_ascii_case("inherit"))
10696                {
10697                    self.advance();
10698                    let parent = self.expect_ident_like()?;
10699                    self.consume_until_statement_boundary();
10700                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10701                        parent,
10702                        detach: true
10703                    }]);
10704                }
10705                self.consume_until_statement_boundary();
10706                Ok(Vec::new())
10707            }
10708            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10709            // single-owner, so there is still nothing to record.
10710            //
10711            // v7.39 (round 652) — but the name now reaches the engine,
10712            // which refuses a role that does not exist as PG does. The
10713            // no-op was swallowing the whole statement, so a dump naming
10714            // a role this server never heard of restored clean and left
10715            // the table owned by whoever ran the restore.
10716            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10717                self.advance();
10718                if matches!(self.peek(), Token::To) {
10719                    self.advance();
10720                }
10721                let role = self.expect_ident_like()?;
10722                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10723                    role
10724                }])
10725            }
10726            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10727            // PG sets a hint; SPG doesn't have clustered storage, so the
10728            // hint itself stays a no-op.
10729            //
10730            // v7.39 (round 652) — the index name is checked now. PG
10731            // errors on one that does not exist, and swallowing that let
10732            // a typo'd CLUSTER ON pass silently.
10733            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10734                self.advance();
10735                // `ON` is a reserved token, not an ident.
10736                if !matches!(self.peek(), Token::On) {
10737                    return Err(self.err(alloc::format!(
10738                        "expected ON after CLUSTER, got {:?}",
10739                        self.peek()
10740                    )));
10741                }
10742                self.advance();
10743                let index = self.expect_ident_like()?;
10744                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10745                    index: Some(index)
10746                }])
10747            }
10748            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10749            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10750            // what a logical decoder puts in the old-tuple image; SPG's
10751            // replication is SQL-text, so there is nothing to record.
10752            // Accept-and-no-op (it used to be a parse error).
10753            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10754                self.advance();
10755                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10756                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10757                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10758                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10759                {
10760                    self.advance(); // IDENTITY
10761                    self.advance(); // USING
10762                    if matches!(self.peek(), Token::Index)
10763                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10764                    {
10765                        self.advance();
10766                    }
10767                    let index = self.expect_ident_like()?;
10768                    self.consume_until_statement_boundary();
10769                    return Ok(alloc::vec![
10770                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10771                    ]);
10772                }
10773                self.consume_until_statement_boundary();
10774                Ok(Vec::new())
10775            }
10776            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10777            //
10778            // v7.39 (round 652) — it used to consume the statement and
10779            // return nothing, on the stated theory that SPG validated at
10780            // ADD CONSTRAINT time so there was never anything left to
10781            // validate. Measured against PG18, ADD CONSTRAINT did not
10782            // scan the existing rows at all — the comment described a
10783            // property SPG did not have, which is why nobody looked. Both
10784            // halves are real now: ADD scans unless told NOT VALID, and
10785            // this scans what NOT VALID skipped.
10786            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10787                self.advance();
10788                self.expect_keyword_ident("constraint")?;
10789                let name = self.expect_ident_like()?;
10790                Ok(alloc::vec![
10791                    crate::ast::AlterTableTarget::ValidateConstraint { name }
10792                ])
10793            }
10794            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10795            // SET (option = value, …). PG uses it to clear per-table
10796            // storage params like fillfactor or autovacuum_*. SPG
10797            // engine-manages those parameters; accept-and-no-op.
10798            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10799                self.advance();
10800                self.consume_until_statement_boundary();
10801                Ok(Vec::new())
10802            }
10803            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10804            // type-of binding (PG 9.0+). SPG composite types
10805            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10806            // TABLE OF is rare and inverse of CREATE TABLE OF.
10807            // Accept-and-no-op until a customer dump round-trips it.
10808            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10809                self.advance();
10810                // v7.39 (round 710) — the type name is validated now.
10811                let type_name = self.expect_ident_like()?;
10812                self.consume_until_statement_boundary();
10813                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10814                    type_name
10815                }])
10816            }
10817            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10818            // (reserved keyword) rather than Token::Ident("not"),
10819            // so it needs its own arm. Accept-and-no-op same as OF.
10820            Token::Not => {
10821                self.advance();
10822                self.consume_until_statement_boundary();
10823                Ok(Vec::new())
10824            }
10825            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10826            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10827                self.advance();
10828                self.expect_row_level_security()?;
10829                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10830                    enabled: None,
10831                    force: Some(true),
10832                }])
10833            }
10834            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10835            Token::Ident(s)
10836                if s.eq_ignore_ascii_case("no")
10837                    && matches!(
10838                        self.tokens.get(self.pos + 1),
10839                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10840                    ) =>
10841            {
10842                self.advance(); // NO
10843                self.advance(); // FORCE
10844                self.expect_row_level_security()?;
10845                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10846                    enabled: None,
10847                    force: Some(false),
10848                }])
10849            }
10850            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10851            // (sets relrowsecurity). The guard requires the next token to be
10852            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10853            Token::Ident(s)
10854                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10855                    && matches!(
10856                        self.tokens.get(self.pos + 1),
10857                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10858                    ) =>
10859            {
10860                let enabled = s.eq_ignore_ascii_case("enable");
10861                self.advance(); // ENABLE/DISABLE
10862                self.expect_row_level_security()?;
10863                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10864                    enabled: Some(enabled),
10865                    force: None,
10866                }])
10867            }
10868            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10869                self.advance();
10870                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10871                // {INDEX|KEY} [name] (cols)`, which every ORM migration
10872                // emits. The same grammar CREATE TABLE already accepts
10873                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
10874                // through the SAME parser — an ALTER-only copy would be a
10875                // second place for the two to drift.
10876                if self.peek_mysql_inline_key_start() {
10877                    return Ok(match self.parse_mysql_inline_key()? {
10878                        Some(c) => {
10879                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
10880                        }
10881                        // FULLTEXT / SPATIAL parse and are accepted as a
10882                        // no-op here exactly as they are inline.
10883                        None => Vec::new(),
10884                    });
10885                }
10886                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
10887                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
10888                // PRIMARY KEY this way; mysqldump emits both.
10889                // Peek-only dispatch (no advance) — `advance()`
10890                // destructively replaces consumed tokens with Eof,
10891                // so saved-pos restore would land on Eofs.
10892                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
10893                {
10894                    // The next-but-one ident is the constraint
10895                    // name; the one after THAT is the kind.
10896                    let kind_pos = self.pos + 2;
10897                    let kind = self.tokens.get(kind_pos).cloned();
10898                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
10899                    {
10900                        let fk = self.parse_table_level_fk()?;
10901                        return Ok(alloc::vec![
10902                            crate::ast::AlterTableTarget::AddForeignKey(fk)
10903                        ]);
10904                    }
10905                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
10906                    {
10907                        self.advance(); // CONSTRAINT
10908                        // v7.39 (read01 round 48) — keep the name; the engine
10909                        // stores it now instead of dropping it on the floor.
10910                        let con_name = self.expect_ident_like()?;
10911                        self.advance(); // PRIMARY
10912                        self.expect_keyword_ident("key")?;
10913                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10914                        // v7.39 (round 711) — the ALTER form carries the
10915                        // timing too (pg_dump writes it here).
10916                        let (deferrable, initially_deferred) =
10917                            self.consume_deferrable_clauses_timed()?;
10918                        return Ok(alloc::vec![
10919                            crate::ast::AlterTableTarget::AddTableConstraint(
10920                                crate::ast::TableConstraint::PrimaryKey {
10921                                    name: Some(con_name),
10922                                    columns: cols,
10923                                    deferrable,
10924                                    initially_deferred,
10925                                }
10926                            )
10927                        ]);
10928                    }
10929                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
10930                    {
10931                        self.advance(); // CONSTRAINT
10932                        // v7.39 (read01 round 48) — keep the name.
10933                        let con_name = self.expect_ident_like()?;
10934                        // v7.22 (mailrs round-13 gap 6) — delegate so
10935                        // the optional `NULLS [NOT] DISTINCT` modifier
10936                        // parses here too (pg_dump emits the ALTER
10937                        // form; semantics enforced by the engine
10938                        // since v7.13).
10939                        let mut uc = self.parse_table_level_unique()?;
10940                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
10941                            *name = Some(con_name);
10942                        }
10943                        return Ok(alloc::vec![
10944                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
10945                        ]);
10946                    }
10947                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
10948                    {
10949                        self.advance(); // CONSTRAINT
10950                        // v7.39 (read01 round 48) — keep the name.
10951                        let con_name = self.expect_ident_like()?;
10952                        self.advance(); // CHECK
10953                        if !matches!(self.peek(), Token::LParen) {
10954                            return Err(self.err(alloc::format!(
10955                                "expected '(' after CHECK, got {:?}", self.peek()
10956                            )));
10957                        }
10958                        self.advance();
10959                        let expr = self.parse_expr(0)?;
10960                        if matches!(self.peek(), Token::RParen) {
10961                            self.advance();
10962                        }
10963                        let not_valid = self.parse_not_valid_suffix();
10964                        return Ok(alloc::vec![
10965                            crate::ast::AlterTableTarget::AddTableConstraint(
10966                                crate::ast::TableConstraint::Check {
10967                                    name: Some(con_name),
10968                                    expr,
10969                                    not_valid,
10970                                }
10971                            )
10972                        ]);
10973                    }
10974                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
10975                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
10976                    // exclusion constraints via this ALTER form.
10977                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
10978                    {
10979                        self.advance(); // CONSTRAINT
10980                        let con_name = self.expect_ident_like()?;
10981                        let mut ex = self.parse_table_level_exclude()?;
10982                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
10983                            *name = Some(con_name);
10984                        }
10985                        return Ok(alloc::vec![
10986                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
10987                        ]);
10988                    }
10989                    // Unknown kind — fall through to FK path which
10990                    // produces a descriptive parse error.
10991                }
10992                let is_fk = matches!(
10993                    self.peek(),
10994                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
10995                        || s.eq_ignore_ascii_case("foreign")
10996                );
10997                if is_fk {
10998                    let fk = self.parse_table_level_fk()?;
10999                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11000                }
11001                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11002                // (no CONSTRAINT prefix) — same dispatch.
11003                match self.peek().clone() {
11004                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11005                        self.advance();
11006                        self.expect_keyword_ident("key")?;
11007                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11008                        let (deferrable, initially_deferred) =
11009                            self.consume_deferrable_clauses_timed()?;
11010                        return Ok(alloc::vec![
11011                            crate::ast::AlterTableTarget::AddTableConstraint(
11012                                crate::ast::TableConstraint::PrimaryKey {
11013                                    name: None,
11014                                    columns: cols,
11015                                    deferrable,
11016                                    initially_deferred,
11017                                }
11018                            )
11019                        ]);
11020                    }
11021                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11022                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
11023                        let uc = self.parse_table_level_unique()?;
11024                        return Ok(alloc::vec![
11025                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11026                        ]);
11027                    }
11028                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11029                    // prefix). The other three bare forms were here and
11030                    // this one was not, so it fell through to the column
11031                    // path and came back as "unexpected reserved keyword
11032                    // 'check' at start of column definition".
11033                    _ if self.peek_table_level_check_start() => {
11034                        let chk = self.parse_table_level_check()?;
11035                        let not_valid = self.parse_not_valid_suffix();
11036                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11037                            unreachable!("parse_table_level_check returns Check")
11038                        };
11039                        return Ok(alloc::vec![
11040                            crate::ast::AlterTableTarget::AddTableConstraint(
11041                                crate::ast::TableConstraint::Check {
11042                                    name: None,
11043                                    expr,
11044                                    not_valid,
11045                                }
11046                            )
11047                        ]);
11048                    }
11049                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11050                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11051                        let ex = self.parse_table_level_exclude()?;
11052                        return Ok(alloc::vec![
11053                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11054                        ]);
11055                    }
11056                    _ => {}
11057                }
11058                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11059                    self.advance();
11060                }
11061                let mut if_not_exists = false;
11062                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11063                    self.advance();
11064                    if !matches!(self.peek(), Token::Not) {
11065                        return Err(self.err(alloc::format!(
11066                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11067                            self.peek()
11068                        )));
11069                    }
11070                    self.advance();
11071                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11072                        return Err(self.err(alloc::format!(
11073                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11074                            self.peek()
11075                        )));
11076                    }
11077                    self.advance();
11078                    if_not_exists = true;
11079                }
11080                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11081                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11082                // returns ColumnDef + an optional inline FK.
11083                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11084                let col_name = column.name.clone();
11085                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11086                    column,
11087                    if_not_exists,
11088                }];
11089                if let Some(mut fk) = col_level_fk {
11090                    if fk.columns.is_empty() {
11091                        fk.columns.push(col_name);
11092                    }
11093                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11094                }
11095                Ok(out)
11096            }
11097            Token::Drop => {
11098                self.advance();
11099                // v7.13.3 — dispatch on the next token. mailrs round-7
11100                // S8 closed DROP COLUMN; round-6 S7 closed
11101                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11102                // RESTRICT modifiers.
11103                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11104                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11105                let subject = match self.peek() {
11106                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11107                        self.advance();
11108                        "constraint"
11109                    }
11110                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11111                        self.advance();
11112                        "column"
11113                    }
11114                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11115                    // `INDEX` lexes as the reserved Token::Index, so it is
11116                    // unambiguous. `KEY` is a plain ident, and PG allows a
11117                    // column literally named "key", so only read it as the
11118                    // keyword when a name follows it.
11119                    Token::Index => {
11120                        self.advance();
11121                        "index"
11122                    }
11123                    Token::Ident(s)
11124                        if s.eq_ignore_ascii_case("key")
11125                            && matches!(
11126                                self.tokens.get(self.pos + 1),
11127                                Some(Token::Ident(_) | Token::QuotedIdent(_))
11128                            ) =>
11129                    {
11130                        self.advance();
11131                        "index"
11132                    }
11133                    // PG-canonical bare `DROP <col>` without COLUMN
11134                    // keyword is also valid; treat any other ident
11135                    // as the column name.
11136                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11137                    other => {
11138                        return Err(self.err(alloc::format!(
11139                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11140                        )));
11141                    }
11142                };
11143                let mut if_exists = false;
11144                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11145                    let n1 = self.tokens.get(self.pos + 1);
11146                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11147                        self.advance();
11148                        self.advance();
11149                        if_exists = true;
11150                    }
11151                }
11152                let name = self.expect_ident_like()?;
11153                let mut cascade = false;
11154                if matches!(
11155                    self.peek(),
11156                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11157                        || s.eq_ignore_ascii_case("restrict")
11158                ) {
11159                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11160                    {
11161                        cascade = true;
11162                    }
11163                    self.advance();
11164                }
11165                if subject == "index" {
11166                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11167                        name,
11168                        if_exists,
11169                    }])
11170                } else if subject == "constraint" {
11171                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11172                        name,
11173                        if_exists,
11174                    }])
11175                } else {
11176                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11177                        column: name,
11178                        if_exists,
11179                        cascade,
11180                    }])
11181                }
11182            }
11183            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11184                self.advance();
11185                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11186                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11187                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11188                // immediately; accept-and-no-op.
11189                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11190                    self.advance();
11191                    self.consume_until_statement_boundary();
11192                    return Ok(Vec::new());
11193                }
11194                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11195                    self.advance();
11196                }
11197                let col_name = self.expect_ident_like()?;
11198                match self.peek() {
11199                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11200                        self.advance();
11201                    }
11202                    // v7.14.0 — pg_dump emits BIGSERIAL via
11203                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11204                    // nextval('seq')` (the sequence is created
11205                    // separately). SPG's BIGSERIAL already uses
11206                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11207                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11208                    // engine no-ops by consuming the tail.
11209                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11210                        // v7.22 (round-13 T2) — `SET DEFAULT
11211                        // nextval('…')` is how pg_dump spells a
11212                        // SERIAL column (plain integer in CREATE
11213                        // TABLE + this ALTER). It used to be
11214                        // swallowed as a no-op, which silently
11215                        // STRIPPED auto-increment from imported
11216                        // schemas — the first post-import INSERT
11217                        // without an explicit id then violated NOT
11218                        // NULL. Lower it to the auto-increment
11219                        // marker instead.
11220                        let is_default_nextval =
11221                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11222                                && matches!(
11223                                    self.tokens.get(self.pos + 2),
11224                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11225                                );
11226                        if is_default_nextval {
11227                            let seq_name = self.scan_sequence_name_until_boundary();
11228                            return Ok(alloc::vec![
11229                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11230                                    column: col_name,
11231                                    seq_name,
11232                                }
11233                            ]);
11234                        }
11235                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11236                        self.advance(); // consume "set"
11237                        match self.peek().clone() {
11238                            Token::Default => {
11239                                self.advance();
11240                                let default_expr = self.parse_expr(0)?;
11241                                return Ok(alloc::vec![
11242                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11243                                        column: col_name,
11244                                        default_expr,
11245                                    }
11246                                ]);
11247                            }
11248                            Token::Not => {
11249                                self.advance();
11250                                if !matches!(self.peek(), Token::Null) {
11251                                    return Err(self.err(alloc::format!(
11252                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11253                                        self.peek()
11254                                    )));
11255                                }
11256                                self.advance();
11257                                return Ok(alloc::vec![
11258                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11259                                        column: col_name,
11260                                    }
11261                                ]);
11262                            }
11263                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11264                            // stored generated column's expression and
11265                            // recompute existing rows.
11266                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11267                                self.advance(); // EXPRESSION
11268                                if matches!(self.peek(), Token::As) {
11269                                    self.advance();
11270                                }
11271                                let expr = self.parse_expr(0)?;
11272                                return Ok(alloc::vec![
11273                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11274                                        column: col_name,
11275                                        expr,
11276                                    }
11277                                ]);
11278                            }
11279                            other => {
11280                                // Other SET subjects (STATISTICS,
11281                                // STORAGE, COMPRESSION, …) stay no-ops —
11282                                // storage hints with no SPG semantics.
11283                                let _ = other;
11284                                self.consume_until_statement_boundary();
11285                                return Ok(Vec::new());
11286                            }
11287                        }
11288                    }
11289                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11290                        self.advance(); // consume "drop"
11291                        return self.parse_alter_column_drop_tail(col_name);
11292                    }
11293                    Token::Drop => {
11294                        self.advance(); // consume Drop token
11295                        return self.parse_alter_column_drop_tail(col_name);
11296                    }
11297                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11298                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11299                        // GENERATED { ALWAYS | BY DEFAULT } AS
11300                        // IDENTITY ( … )`: pg_dump's spelling for
11301                        // identity columns. Same auto-increment
11302                        // lowering as the nextval default; the
11303                        // sequence options inside the parens are
11304                        // no-ops under SPG's max+1 semantics.
11305                        let is_generated = matches!(
11306                            self.tokens.get(self.pos + 1),
11307                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11308                        );
11309                        if !is_generated {
11310                            return Err(self.err(alloc::format!(
11311                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11312                                self.tokens.get(self.pos + 1)
11313                            )));
11314                        }
11315                        let seq_name = self.scan_sequence_name_until_boundary();
11316                        return Ok(alloc::vec![
11317                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11318                                column: col_name,
11319                                seq_name,
11320                            }
11321                        ]);
11322                    }
11323                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11324                    // column: floor the next allocated value at n (bare
11325                    // RESTART = restart from the start value, 1).
11326                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11327                        self.advance();
11328                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11329                        {
11330                            self.advance();
11331                            let neg = if matches!(self.peek(), Token::Minus) {
11332                                self.advance();
11333                                true
11334                            } else {
11335                                false
11336                            };
11337                            match self.advance() {
11338                                Token::Integer(v) => Some(if neg { -v } else { v }),
11339                                other => {
11340                                    return Err(self.err(alloc::format!(
11341                                        "expected integer after RESTART WITH, got {other:?}"
11342                                    )));
11343                                }
11344                            }
11345                        } else {
11346                            None
11347                        };
11348                        return Ok(alloc::vec![
11349                            crate::ast::AlterTableTarget::AlterColumnRestart {
11350                                column: col_name,
11351                                with,
11352                            }
11353                        ]);
11354                    }
11355                    other => {
11356                        return Err(self.err(alloc::format!(
11357                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11358                        )));
11359                    }
11360                }
11361                // v7.39 (round 713) — the type parser has consumed a
11362                // trailing `COLLATE <name>` since Phase 2.5, and
11363                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11364                // TYPE text COLLATE "C"` parsed clean and changed
11365                // nothing. Keep the clause; the engine re-collates.
11366                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11367                    self.parse_type_with_implied_flags()?;
11368                let collation = if coll_explicit {
11369                    coll_name.map(|n| (coll, n))
11370                } else {
11371                    None
11372                };
11373                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11374                {
11375                    self.advance();
11376                    Some(self.parse_expr(0)?)
11377                } else {
11378                    None
11379                };
11380                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11381                    column: col_name,
11382                    new_type,
11383                    using,
11384                    collation,
11385                }])
11386            }
11387            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11388            // PG also supports `RENAME TO new_table` for table-name
11389            // rename; that surface is deferred (pg_dump never emits
11390            // it). If the first post-RENAME ident is `TO`, the user
11391            // is asking for table rename — error with a clear
11392            // message rather than misparsing `TO` as a column name.
11393            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11394                self.advance();
11395                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11396                // table-name rename (mailrs round-10 A.5 — used
11397                // by migrate-042's `RENAME TO email_contacts`).
11398                // `TO` lexes as Token::To.
11399                if matches!(self.peek(), Token::To)
11400                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11401                {
11402                    self.advance();
11403                    let new = self.expect_ident_like()?;
11404                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11405                        new,
11406                    }]);
11407                }
11408                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11409                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11410                    self.advance();
11411                    let old = self.expect_ident_like()?;
11412                    if matches!(self.peek(), Token::To) {
11413                        self.advance();
11414                    } else {
11415                        self.expect_keyword_ident("to")?;
11416                    }
11417                    let new = self.expect_ident_like()?;
11418                    return Ok(alloc::vec![
11419                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11420                    ]);
11421                }
11422                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11423                    self.advance();
11424                }
11425                let old = self.expect_ident_like()?;
11426                // `TO` is a reserved keyword token; accept both
11427                // Token::To and Token::Ident("to") for consistency.
11428                if matches!(self.peek(), Token::To) {
11429                    self.advance();
11430                } else {
11431                    self.expect_keyword_ident("to")?;
11432                }
11433                let new = self.expect_ident_like()?;
11434                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11435                    old,
11436                    new,
11437                }])
11438            }
11439            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11440            // { ALL | <name> }`. pg_dump --disable-triggers wraps
11441            // every data block with these. Real disable semantics —
11442            // not no-op — because reload correctness assumes the
11443            // triggers don't fire (rows already carry their
11444            // computed values from prod).
11445            Token::Ident(s)
11446                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11447            {
11448                let enabled = s.eq_ignore_ascii_case("enable");
11449                self.advance();
11450                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11451                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11452                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11453                // pg_dump output) — anything else falls through to
11454                // the catch-all error below.
11455                // v7.22 (round-13 T3) — mysqldump wraps every data
11456                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11457                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11458                // maintains indexes incrementally — engine no-op.
11459                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11460                    self.advance();
11461                    return Ok(Vec::new());
11462                }
11463                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11464                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11465                // to gate triggers on session_replication_role; SPG
11466                // has no replica role, so the prefix is consumed and
11467                // treated identically to the plain ENABLE/DISABLE
11468                // TRIGGER form.
11469                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11470                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11471                {
11472                    self.advance();
11473                }
11474                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11475                    return Err(self.err(alloc::format!(
11476                        "expected TRIGGER after {}, got {:?}",
11477                        if enabled { "ENABLE" } else { "DISABLE" },
11478                        self.peek()
11479                    )));
11480                }
11481                self.advance();
11482                // `ALL` lexes as Token::All (reserved); also
11483                // accept Token::Ident("all") for symmetry.
11484                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11485                // TRIGGER selectors. USER (= all user triggers) is
11486                // semantically ALL here; REPLICA / ALWAYS gate on
11487                // session_replication_role which SPG doesn't track.
11488                // All map to TriggerSelector::All.
11489                let which = if matches!(self.peek(), Token::All)
11490                    || matches!(self.peek(), Token::Ident(s)
11491                        if s.eq_ignore_ascii_case("all")
11492                            || s.eq_ignore_ascii_case("user")
11493                            || s.eq_ignore_ascii_case("replica")
11494                            || s.eq_ignore_ascii_case("always"))
11495                {
11496                    self.advance();
11497                    crate::ast::TriggerSelector::All
11498                } else {
11499                    let name = self.expect_ident_like()?;
11500                    crate::ast::TriggerSelector::Named(name)
11501                };
11502                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11503                    which,
11504                    enabled,
11505                }])
11506            }
11507            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11508            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11509                self.advance();
11510                if !matches!(self.peek(), Token::Partition)
11511                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11512                        if s.eq_ignore_ascii_case("partition"))
11513                {
11514                    return Err(self.err(alloc::format!(
11515                        "expected PARTITION after ATTACH, got {:?}",
11516                        self.peek()
11517                    )));
11518                }
11519                self.advance();
11520                let child = self.expect_ident_like()?;
11521                let bounds = self.parse_partition_bounds_tail()?;
11522                Ok(alloc::vec![
11523                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11524                ])
11525            }
11526            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11527            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11528                self.advance();
11529                if !matches!(self.peek(), Token::Partition)
11530                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11531                        if s.eq_ignore_ascii_case("partition"))
11532                {
11533                    return Err(self.err(alloc::format!(
11534                        "expected PARTITION after DETACH, got {:?}",
11535                        self.peek()
11536                    )));
11537                }
11538                self.advance();
11539                let child = self.expect_ident_like()?;
11540                let mut concurrently = false;
11541                let mut finalize = false;
11542                loop {
11543                    match self.peek().clone() {
11544                        Token::Ident(s) | Token::QuotedIdent(s)
11545                            if s.eq_ignore_ascii_case("concurrently") =>
11546                        {
11547                            self.advance();
11548                            concurrently = true;
11549                        }
11550                        Token::Ident(s) | Token::QuotedIdent(s)
11551                            if s.eq_ignore_ascii_case("finalize") =>
11552                        {
11553                            self.advance();
11554                            finalize = true;
11555                        }
11556                        _ => break,
11557                    }
11558                }
11559                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11560                    child,
11561                    concurrently,
11562                    finalize,
11563                }])
11564            }
11565            other => Err(self.err(alloc::format!(
11566                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11567            ))),
11568        }
11569    }
11570
11571    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11572    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11573    /// TABLE … ATTACH PARTITION. Shares the same grammar as
11574    /// `parse_partition_of_tail`'s bounds branch.
11575    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11576    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11577    /// lowering each to the respective AlterTableTarget. Any
11578    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11579    /// no-op via consume_until_statement_boundary.
11580    fn parse_alter_column_drop_tail(
11581        &mut self,
11582        col_name: String,
11583    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11584        match self.peek().clone() {
11585            Token::Default => {
11586                self.advance();
11587                Ok(alloc::vec![
11588                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11589                ])
11590            }
11591            Token::Not => {
11592                self.advance();
11593                if !matches!(self.peek(), Token::Null) {
11594                    return Err(self.err(alloc::format!(
11595                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11596                        self.peek()
11597                    )));
11598                }
11599                self.advance();
11600                Ok(alloc::vec![
11601                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11602                ])
11603            }
11604            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11605            // generated column into a plain column.
11606            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11607                self.advance();
11608                // v7.39 (round 187, U10) — IF EXISTS was consumed but
11609                // dropped, so the engine still errored on a plain
11610                // column; PG's semantics are NOTICE + skip.
11611                let mut if_exists = false;
11612                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11613                    self.advance();
11614                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11615                        self.advance();
11616                        if_exists = true;
11617                    }
11618                }
11619                Ok(alloc::vec![
11620                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
11621                        column: col_name,
11622                        if_exists,
11623                    }
11624                ])
11625            }
11626            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11627            // identity column into a plain column.
11628            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11629                self.advance();
11630                let mut if_exists = false;
11631                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11632                    self.advance();
11633                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11634                        self.advance();
11635                        if_exists = true;
11636                    }
11637                }
11638                Ok(alloc::vec![
11639                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11640                        column: col_name,
11641                        if_exists,
11642                    }
11643                ])
11644            }
11645            _ => {
11646                self.consume_until_statement_boundary();
11647                Ok(Vec::new())
11648            }
11649        }
11650    }
11651
11652    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11653    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11654    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11655    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11656    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11657        let mut opts = crate::ast::CopyOptions::default();
11658        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11659            return Ok(opts);
11660        }
11661        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11662            self.advance();
11663        }
11664        if matches!(self.peek(), Token::LParen) {
11665            self.advance();
11666            loop {
11667                self.parse_one_copy_option(&mut opts)?;
11668                match self.peek() {
11669                    Token::Comma => {
11670                        self.advance();
11671                    }
11672                    Token::RParen => {
11673                        self.advance();
11674                        break;
11675                    }
11676                    other => {
11677                        return Err(self.err(alloc::format!(
11678                            "expected ',' or ')' in COPY options, got {other:?}"
11679                        )));
11680                    }
11681                }
11682            }
11683        } else {
11684            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11685                self.parse_one_copy_option(&mut opts)?;
11686            }
11687        }
11688        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11689            return Err(self.err(alloc::format!(
11690                "unexpected token after COPY options: {:?}",
11691                self.peek()
11692            )));
11693        }
11694        Ok(opts)
11695    }
11696
11697    fn parse_one_copy_option(
11698        &mut self,
11699        opts: &mut crate::ast::CopyOptions,
11700    ) -> Result<(), ParseError> {
11701        use crate::ast::CopyFormat;
11702        // The option keyword. NULL lexes as its own token; the rest are
11703        // bare identifiers.
11704        let kw = match self.advance() {
11705            Token::Null => alloc::string::String::from("NULL"),
11706            Token::Ident(s) => s.to_uppercase(),
11707            other => {
11708                return Err(self.err(alloc::format!(
11709                    "expected a COPY option keyword, got {other:?}"
11710                )));
11711            }
11712        };
11713        match kw.as_str() {
11714            "FORMAT" => {
11715                let fmt = self.expect_ident_like()?;
11716                match fmt.to_ascii_uppercase().as_str() {
11717                    "CSV" => opts.format = CopyFormat::Csv,
11718                    "TEXT" => opts.format = CopyFormat::Text,
11719                    other => {
11720                        return Err(self.err(alloc::format!(
11721                            "COPY format \"{}\" not recognized",
11722                            other.to_ascii_lowercase()
11723                        )));
11724                    }
11725                }
11726            }
11727            // Legacy bare format keywords.
11728            "CSV" => opts.format = CopyFormat::Csv,
11729            "TEXT" => opts.format = CopyFormat::Text,
11730            "HEADER" => {
11731                opts.header = match self.peek() {
11732                    Token::True => {
11733                        self.advance();
11734                        true
11735                    }
11736                    Token::False => {
11737                        self.advance();
11738                        false
11739                    }
11740                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11741                        self.advance();
11742                        true
11743                    }
11744                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11745                        self.advance();
11746                        false
11747                    }
11748                    // Bare HEADER (no boolean) means HEADER true.
11749                    _ => true,
11750                };
11751            }
11752            // r1066 (7.38 S5.1) — pgbench 14+ loads with
11753            // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
11754            // vacuum bookkeeping on a freshly created/truncated
11755            // table; SPG's per-statement visibility makes it a
11756            // faithful no-op, and rejecting it aborted `pgbench -i`
11757            // against the drop-in. Accept ON/OFF/bare, change nothing.
11758            "FREEZE" => match self.peek() {
11759                Token::True | Token::False => {
11760                    self.advance();
11761                }
11762                Token::Ident(s)
11763                    if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
11764                {
11765                    self.advance();
11766                }
11767                _ => {}
11768            },
11769            "DELIMITER" | "QUOTE" | "ESCAPE" => {
11770                let s = match self.advance() {
11771                    Token::String(s) => s,
11772                    other => {
11773                        return Err(self.err(alloc::format!(
11774                            "COPY {kw} expects a single-character string, got {other:?}"
11775                        )));
11776                    }
11777                };
11778                // v7.39 (round 247) — PG's wording (0A000), keyword in
11779                // lowercase: "COPY delimiter must be a single one-byte
11780                // character".
11781                let one_byte_err = || {
11782                    self.err(alloc::format!(
11783                        "COPY {} must be a single one-byte character",
11784                        kw.to_ascii_lowercase()
11785                    ))
11786                };
11787                let mut chars = s.chars();
11788                let c = chars.next().ok_or_else(one_byte_err)?;
11789                if chars.next().is_some() || c.len_utf8() != 1 {
11790                    return Err(one_byte_err());
11791                }
11792                match kw.as_str() {
11793                    "DELIMITER" => opts.delimiter = Some(c),
11794                    "QUOTE" => opts.quote = Some(c),
11795                    _ => opts.escape = Some(c),
11796                }
11797            }
11798            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11799            "FORCE_QUOTE" => {
11800                if matches!(self.peek(), Token::Star) {
11801                    self.advance();
11802                    opts.force_quote = Some(Vec::new());
11803                } else {
11804                    if !matches!(self.peek(), Token::LParen) {
11805                        return Err(self.err(alloc::format!(
11806                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11807                            self.peek()
11808                        )));
11809                    }
11810                    self.advance();
11811                    let mut cols = Vec::new();
11812                    loop {
11813                        cols.push(self.expect_ident_like()?);
11814                        match self.peek() {
11815                            Token::Comma => {
11816                                self.advance();
11817                            }
11818                            Token::RParen => {
11819                                self.advance();
11820                                break;
11821                            }
11822                            other => {
11823                                return Err(self.err(alloc::format!(
11824                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11825                                )));
11826                            }
11827                        }
11828                    }
11829                    opts.force_quote = Some(cols);
11830                }
11831            }
11832            "NULL" => {
11833                opts.null_str = Some(match self.advance() {
11834                    Token::String(s) => s,
11835                    other => {
11836                        return Err(self.err(alloc::format!(
11837                            "COPY NULL expects a quoted string, got {other:?}"
11838                        )));
11839                    }
11840                });
11841            }
11842            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11843            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11844            // FORCE_NULL too.
11845            "FORCE_NOT_NULL" | "FORCE_NULL" => {
11846                let cols = self.parse_copy_column_list(&kw)?;
11847                if kw == "FORCE_NOT_NULL" {
11848                    opts.force_not_null = Some(cols);
11849                } else {
11850                    opts.force_null = Some(cols);
11851                }
11852            }
11853            other => {
11854                // PG's wording, lowercased option name.
11855                return Err(self.err(alloc::format!(
11856                    "option \"{}\" not recognized",
11857                    other.to_ascii_lowercase()
11858                )));
11859            }
11860        }
11861        Ok(())
11862    }
11863
11864    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11865    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11866    /// is the `*` spelling.
11867    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11868        if matches!(self.peek(), Token::Star) {
11869            self.advance();
11870            return Ok(Vec::new());
11871        }
11872        if !matches!(self.peek(), Token::LParen) {
11873            return Err(self.err(alloc::format!(
11874                "expected '(' or '*' after {kw}, got {:?}",
11875                self.peek()
11876            )));
11877        }
11878        self.advance();
11879        let mut cols = Vec::new();
11880        loop {
11881            cols.push(self.expect_ident_like()?);
11882            match self.peek() {
11883                Token::Comma => {
11884                    self.advance();
11885                }
11886                Token::RParen => {
11887                    self.advance();
11888                    break;
11889                }
11890                other => {
11891                    return Err(self.err(alloc::format!(
11892                        "expected ',' or ')' in {kw} list, got {other:?}"
11893                    )));
11894                }
11895            }
11896        }
11897        Ok(cols)
11898    }
11899
11900    fn parse_partition_bounds_tail(
11901        &mut self,
11902    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
11903        use crate::ast::PartitionOfBoundsAst;
11904        match self.peek() {
11905            Token::Default => {
11906                self.advance();
11907                Ok(PartitionOfBoundsAst::Default)
11908            }
11909            Token::For => {
11910                self.advance();
11911                if !matches!(self.peek(), Token::Values) {
11912                    return Err(
11913                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
11914                    );
11915                }
11916                self.advance();
11917                let want_with = matches!(
11918                    self.peek(),
11919                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
11920                );
11921                if want_with {
11922                    self.advance();
11923                    if !matches!(self.peek(), Token::LParen) {
11924                        return Err(self.err(format!(
11925                            "expected '(' after FOR VALUES WITH, got {:?}",
11926                            self.peek()
11927                        )));
11928                    }
11929                    self.advance();
11930                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
11931                    loop {
11932                        let key = self.expect_ident_like()?;
11933                        let n = match self.peek().clone() {
11934                            Token::Integer(v) if u32::try_from(v).is_ok() => {
11935                                self.advance();
11936                                v as u32
11937                            }
11938                            other => {
11939                                return Err(self.err(format!(
11940                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
11941                                )));
11942                            }
11943                        };
11944                        match key.to_ascii_uppercase().as_str() {
11945                            "MODULUS" => modulus = Some(n),
11946                            "REMAINDER" => remainder = Some(n),
11947                            other => {
11948                                return Err(self.err(format!(
11949                                    "FOR VALUES WITH: unknown key {other:?}; \
11950                                     expected MODULUS or REMAINDER"
11951                                )));
11952                            }
11953                        }
11954                        match self.peek() {
11955                            Token::Comma => {
11956                                self.advance();
11957                            }
11958                            Token::RParen => {
11959                                self.advance();
11960                                break;
11961                            }
11962                            other => {
11963                                return Err(self.err(format!(
11964                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
11965                                )));
11966                            }
11967                        }
11968                    }
11969                    let modulus = modulus
11970                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
11971                    let remainder = remainder.ok_or_else(|| {
11972                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
11973                    })?;
11974                    if modulus == 0 {
11975                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
11976                    }
11977                    if remainder >= modulus {
11978                        return Err(self.err(format!(
11979                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
11980                        )));
11981                    }
11982                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
11983                }
11984                match self.peek() {
11985                    Token::From => {
11986                        self.advance();
11987                        let lower = Box::new(self.parse_partition_bound_expr()?);
11988                        if !matches!(self.peek(), Token::To) {
11989                            return Err(self.err(format!(
11990                                "expected TO after FROM (...), got {:?}",
11991                                self.peek()
11992                            )));
11993                        }
11994                        self.advance();
11995                        let upper = Box::new(self.parse_partition_bound_expr()?);
11996                        Ok(PartitionOfBoundsAst::Range { lower, upper })
11997                    }
11998                    Token::In => {
11999                        self.advance();
12000                        if !matches!(self.peek(), Token::LParen) {
12001                            return Err(self.err(format!(
12002                                "expected '(' after FOR VALUES IN, got {:?}",
12003                                self.peek()
12004                            )));
12005                        }
12006                        self.advance();
12007                        let mut values = Vec::new();
12008                        loop {
12009                            values.push(self.parse_expr(0)?);
12010                            match self.peek() {
12011                                Token::Comma => {
12012                                    self.advance();
12013                                }
12014                                Token::RParen => {
12015                                    self.advance();
12016                                    break;
12017                                }
12018                                other => {
12019                                    return Err(self.err(format!(
12020                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12021                                    )));
12022                                }
12023                            }
12024                        }
12025                        if values.is_empty() {
12026                            return Err(
12027                                self.err("FOR VALUES IN requires at least one literal".to_string())
12028                            );
12029                        }
12030                        Ok(PartitionOfBoundsAst::List { values })
12031                    }
12032                    other => Err(self.err(format!(
12033                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12034                    ))),
12035                }
12036            }
12037            other => Err(self.err(format!(
12038                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12039            ))),
12040        }
12041    }
12042
12043    /// v7.16.2 — peek for `information_schema.<tbl>` /
12044    /// `pg_catalog.<tbl>` triples and, if matched, consume all
12045    /// three tokens + return a synthetic table name the engine's
12046    /// SELECT path recognises as a virtual view. Returns `None`
12047    /// when the head doesn't look like a meta-qualified name.
12048    /// Used by `parse_table_ref` to bypass the
12049    /// `expect_ident_like` schema-strip for these specific PG
12050    /// meta schemas (mailrs round-10 A.3).
12051    fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12052        // Extract the schema name. Must be a plain ident token.
12053        let schema = match self.tokens.get(self.pos) {
12054            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12055            _ => return None,
12056        };
12057        // Dot.
12058        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12059            return None;
12060        }
12061        // The table-side ident may lex as a reserved keyword
12062        // (e.g. `Token::Tables`). Tolerate the common ones via a
12063        // helper that reads the trailing token's underlying name.
12064        let tbl = match self.tokens.get(self.pos + 2)? {
12065            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12066            Token::Tables => "tables".to_string(),
12067            // Other PG meta table names that may collide with
12068            // reserved keywords land here as needed.
12069            _ => return None,
12070        };
12071        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12072        // names so the synthetic name doesn't double-prefix
12073        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12074        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12075            ("__spg_info_", tbl.to_ascii_lowercase())
12076        } else if schema.eq_ignore_ascii_case("pg_catalog") {
12077            // v7.39 (round 541) — only the catalogs SPG actually
12078            // synthesises are rewritten, which is what the BARE path
12079            // has always checked. Anything else keeps its own name and
12080            // takes the ordinary route: `pg_stat_activity` and friends
12081            // resolve through meta_view_result, and a name that is no
12082            // catalog at all gets PG's "relation does not exist"
12083            // instead of a message about a view SPG cannot materialise.
12084            let lowered = tbl.to_ascii_lowercase();
12085            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12086                self.advance(); // schema
12087                self.advance(); // dot
12088                self.advance(); // tbl
12089                return Some((lowered.clone(), lowered));
12090            }
12091            let bare = lowered
12092                .strip_prefix("pg_")
12093                .map(alloc::string::String::from)
12094                .unwrap_or(lowered);
12095            ("__spg_pg_", bare)
12096        } else if schema.eq_ignore_ascii_case("mysql") {
12097            // v7.17.0 Phase 3.P0-65 — MySQL system schema
12098            // (`mysql.user`, `mysql.db`). Same synthetic-name
12099            // shape as pg_catalog.
12100            ("__spg_mysql_", tbl.to_ascii_lowercase())
12101        } else {
12102            return None;
12103        };
12104        self.advance(); // schema
12105        self.advance(); // dot
12106        self.advance(); // tbl
12107        Some((
12108            alloc::format!("{prefix}{normalised}"),
12109            tbl.to_ascii_lowercase(),
12110        ))
12111    }
12112
12113    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12114    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12115    /// implicit front of every search_path, so a bare reference to a
12116    /// known catalog table always means the catalog table. Only the
12117    /// names the engine actually synthesises are recognised — any
12118    /// other `pg_*` ident stays a user table (mailrs embed round-12).
12119    fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12120        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12121        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12122        // `pg_catalog` at the front of every search_path. (pg_stat_activity
12123        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12124        // through the meta_view_result path instead, and already resolve
12125        // bare — they must NOT be listed here or the __spg_ rewrite would
12126        // mis-target them.)
12127        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12128        let name = match self.tokens.get(self.pos) {
12129            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12130            _ => return None,
12131        };
12132        // A following dot means this ident is a schema qualifier,
12133        // not a table name — let the qualified path handle it.
12134        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12135            return None;
12136        }
12137        if !PG_META_TABLES.contains(&name.as_str()) {
12138            return None;
12139        }
12140        self.advance();
12141        let bare = name.strip_prefix("pg_").unwrap_or(&name);
12142        Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12143    }
12144
12145    /// Consume a bare ident if its lowercase matches `kw`, else err.
12146    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12147    /// Peeks only; the caller advances.
12148    fn peek_keyword_ident(&self, kw: &str) -> bool {
12149        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12150    }
12151
12152    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12153        match self.advance() {
12154            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12155            other => Err(ParseError {
12156                message: format!("expected {kw:?}, got {other:?}"),
12157                token_pos: self.consumed_pos(),
12158            }),
12159        }
12160    }
12161
12162    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12163    /// literal (`'foo'`) — same shape used by CREATE USER for the
12164    /// username slot.
12165    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12166        match self.advance() {
12167            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12168            other => Err(ParseError {
12169                message: format!("expected identifier or string, got {other:?}"),
12170                token_pos: self.consumed_pos(),
12171            }),
12172        }
12173    }
12174
12175    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12176        match self.advance() {
12177            Token::String(s) => Ok(s),
12178            other => Err(ParseError {
12179                message: format!("expected quoted string, got {other:?}"),
12180                token_pos: self.consumed_pos(),
12181            }),
12182        }
12183    }
12184
12185    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12186        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12187        // subqueries recurse through here without passing
12188        // parse_expr; share the same nesting budget.
12189        self.enter_nested()?;
12190        let r = self.parse_select_stmt_inner();
12191        self.nest_depth -= 1;
12192        r
12193    }
12194
12195    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12196        // Caller dispatches on Token::Select; the inner helper handles
12197        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12198        // get a fresh bare-select parse and may not have their own ORDER
12199        // BY / LIMIT.
12200        let mut head = self.parse_bare_select()?;
12201        self.parse_setop_chain_into(&mut head)?;
12202        self.parse_select_tail_into(&mut head)?;
12203        Ok(Statement::Select(head))
12204    }
12205
12206    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12207    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12208    /// token), and INTERSECT [ALL] (a bare ident — it was never
12209    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12210    /// tighter than UNION / EXCEPT — the executor folds the chain
12211    /// left-to-right, which is already correct for LEADING
12212    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12213    /// pair nests into that previous peer, so A UNION B INTERSECT C
12214    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12215    /// groups.
12216    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12217        // A parenthesized group arrives with its own (already
12218        // regrouped) unions on `head`; only the pairs THIS chain
12219        // appends participate in the precedence regroup below —
12220        // nesting an outer INTERSECT into a group-internal peer
12221        // would dissolve the explicit grouping.
12222        let boundary = head.unions.len();
12223        loop {
12224            let base = match self.peek() {
12225                Token::Union => UnionKind::Distinct,
12226                Token::Except => UnionKind::Except,
12227                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12228                _ => break,
12229            };
12230            self.advance();
12231            let kind = if matches!(self.peek(), Token::All) {
12232                self.advance();
12233                match base {
12234                    UnionKind::Distinct => UnionKind::All,
12235                    UnionKind::Except => UnionKind::ExceptAll,
12236                    _ => UnionKind::IntersectAll,
12237                }
12238            } else {
12239                base
12240            };
12241            let peer = self.parse_bare_select()?;
12242            head.unions.push((kind, peer));
12243        }
12244        let mut pairs = core::mem::take(&mut head.unions);
12245        let tail = pairs.split_off(boundary);
12246        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12247        for (kind, peer) in tail {
12248            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12249            // An intersect nests into the previous element of THIS
12250            // chain only; with no new previous element it stays at
12251            // the outer level (the left fold applies it to the
12252            // whole head, group included).
12253            match (
12254                is_intersect,
12255                regrouped.len() > boundary,
12256                regrouped.last_mut(),
12257            ) {
12258                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12259                _ => regrouped.push((kind, peer)),
12260            }
12261        }
12262        head.unions = regrouped;
12263        Ok(())
12264    }
12265
12266    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12267    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12268    /// the top-level bare VALUES statement reuses it verbatim.
12269    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12270    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12271    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12272    /// where the grouping-set universe is still in scope.
12273    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12274        if !matches!(self.peek(), Token::Order) {
12275            return Ok(Vec::new());
12276        }
12277        self.advance();
12278        if !self.peek_is_by() {
12279            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12280        }
12281        self.advance();
12282        let mut keys = Vec::new();
12283        loop {
12284            // v7.39 (round 691) — save/restore, the discipline this parser
12285            // already uses around `pending_sample_preds`, so a subquery inside
12286            // a key neither inherits nor leaks the channel.
12287            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12288            let saved_coll = self.order_key_collation.take();
12289            let parsed = self.parse_expr(0);
12290            self.in_order_by_key = saved_flag;
12291            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12292            let expr = parsed?;
12293            let desc = if matches!(self.peek(), Token::Desc) {
12294                self.advance();
12295                true
12296            } else if matches!(self.peek(), Token::Asc) {
12297                self.advance();
12298                false
12299            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12300                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12301                // one ordering per type, so the btree comparison operators map
12302                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12303                // would need a custom operator class — honest error.
12304                self.advance();
12305                match self.advance() {
12306                    Token::Lt | Token::LtEq => false,
12307                    Token::Gt | Token::GtEq => true,
12308                    other => {
12309                        return Err(self.err(alloc::format!(
12310                            "ORDER BY USING supports the btree comparison \
12311                             operators (< <= > >=); got {other:?}"
12312                        )));
12313                    }
12314                }
12315            } else {
12316                false
12317            };
12318            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12319            let nulls_first = self.parse_optional_nulls_placement()?;
12320            keys.push(OrderBy {
12321                expr,
12322                desc,
12323                nulls_first,
12324                collation,
12325            });
12326            if matches!(self.peek(), Token::Comma) {
12327                self.advance();
12328            } else {
12329                break;
12330            }
12331        }
12332        Ok(keys)
12333    }
12334
12335    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12336        // v7.39 (round 135) — a grouping-set query may have already parsed +
12337        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12338        // no ORDER BY token is present, keep that pre-set order_by rather than
12339        // clobbering it with an empty list.
12340        let parsed_keys = self.parse_order_by_keys()?;
12341        head.order_by = if parsed_keys.is_empty() {
12342            core::mem::take(&mut head.order_by)
12343        } else {
12344            parsed_keys
12345        };
12346        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12347        // order. PG's grammar takes a limit clause and an offset clause
12348        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12349        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12350        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12351        // spelling died on `expected end of input, got Limit`.
12352        //
12353        // Each may appear at most once, and LIMIT and FETCH FIRST are
12354        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12355        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12356        // A second one is left unconsumed here, which the caller reports
12357        // as trailing input rather than silently taking the last.
12358        let mut saw_limit = false;
12359        let mut saw_offset = false;
12360        loop {
12361            if !saw_limit && matches!(self.peek(), Token::Limit) {
12362                self.advance();
12363                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12364                // PG synonyms for "no limit". Treat both as None
12365                // (no head.limit set) so the engine's existing
12366                // unlimited-result path takes over. Reject was the
12367                // pre-5.1 behaviour and broke pg_dump-flavoured
12368                // tooling that occasionally emits LIMIT NULL.
12369                if self.consume_limit_unbounded_sentinel() {
12370                    head.limit = None;
12371                } else {
12372                    let first = self.parse_limit_expr("LIMIT")?;
12373                    // MySQL `LIMIT offset, count` — the first number is
12374                    // the offset when a comma follows.
12375                    if matches!(self.peek(), Token::Comma) {
12376                        self.advance();
12377                        let count = self.parse_limit_expr("LIMIT")?;
12378                        head.offset = Some(first);
12379                        saw_offset = true;
12380                        head.limit = Some(count);
12381                    } else {
12382                        head.limit = Some(first);
12383                    }
12384                }
12385                saw_limit = true;
12386                continue;
12387            }
12388            if !saw_offset && matches!(self.peek(), Token::Offset) {
12389                self.advance();
12390                // PG also accepts an optional `ROW` / `ROWS` trailer
12391                // after the offset value (`OFFSET 10 ROWS`). The
12392                // FETCH-FIRST branch below relies on the same.
12393                let off = self.parse_limit_expr("OFFSET")?;
12394                self.consume_optional_rows_keyword();
12395                head.offset = Some(off);
12396                saw_offset = true;
12397                continue;
12398            }
12399            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12400            // the SQL-standard alias for LIMIT. PG accepts both
12401            // spellings interchangeably; pg_dump emits FETCH FIRST in
12402            // newer versions. We map it onto `head.limit` so the
12403            // engine path is unified.
12404            if !saw_limit
12405                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12406                    if s.eq_ignore_ascii_case("fetch"))
12407            {
12408                self.advance(); // FETCH
12409                // `FIRST` or `NEXT` (both legal per SQL standard).
12410                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12411                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12412                {
12413                    self.advance();
12414                }
12415                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12416                // implicit 1 — but we always consume one if present).
12417                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12418                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12419                {
12420                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12421                    crate::ast::LimitExpr::Literal(1)
12422                } else {
12423                    self.parse_limit_expr("FETCH FIRST")?
12424                };
12425                // Eat `ROW` / `ROWS` if not already consumed above.
12426                self.consume_optional_rows_keyword();
12427                // Optional `ONLY` (the spec form) — or the SQL:2008
12428                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12429                // now honours WITH TIES by extending past the LIMIT
12430                // truncation point through every row that shares the
12431                // last-kept row's ORDER BY key.
12432                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12433                    if s.eq_ignore_ascii_case("only"))
12434                {
12435                    self.advance();
12436                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12437                    if s.eq_ignore_ascii_case("with"))
12438                {
12439                    self.advance(); // WITH
12440                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12441                        if s.eq_ignore_ascii_case("ties"))
12442                    {
12443                        self.advance();
12444                        head.limit_with_ties = true;
12445                    }
12446                }
12447                head.limit = Some(count);
12448                saw_limit = true;
12449                continue;
12450            }
12451            break;
12452        }
12453        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12454        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12455        //       [ OF table_name [, …] ]
12456        //       [ NOWAIT | SKIP LOCKED ]
12457        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12458        // FOR SHARE OF t2`). SPG is a single-writer engine — every
12459        // SELECT already returns a consistent snapshot — so these
12460        // are accept-and-discard: the parser absorbs them so
12461        // mailrs / Rails / Django code paths that emit `SELECT
12462        // … FOR UPDATE` for advisory pessimistic locking load
12463        // without a parser error. The on-disk locking model is
12464        // unchanged; callers that rely on FOR UPDATE for read-
12465        // through-write ordering still get the right answer
12466        // because SPG serialises writes anyway.
12467        head.locking = self
12468            .consume_optional_for_lock_clauses()
12469            .map(alloc::boxed::Box::new);
12470        Ok(())
12471    }
12472
12473    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12474    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12475    /// LOCKED ]` trailers. Each clause is fully accepted and
12476    /// discarded — SPG's single-writer model already satisfies the
12477    /// callers' implicit ordering requirement. Stops at the first
12478    /// token that isn't `FOR`.
12479    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12480        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12481        // not discarded. PG keeps the strongest of several clauses; the
12482        // policy of the last one wins, which is what this loop records.
12483        let mut seen: Option<crate::ast::LockingClause> = None;
12484        while matches!(self.peek(), Token::For) {
12485            // v7.37.14 (A2.5-stub) — record that this query asked
12486            // for a row lock the parser is about to silently
12487            // discard. Operators surface the count via
12488            // `spg_sql::silent_for_update_count()` so they can
12489            // gauge how much of the workload depends on advisory
12490            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12491            // before v7.37.15's per-row tuple locking lands.
12492            crate::record_silent_for_update_clause();
12493            self.advance(); // FOR
12494            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12495            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12496            let mut no_key = false;
12497            let mut key = false;
12498            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12499                if s.eq_ignore_ascii_case("no"))
12500            {
12501                self.advance(); // NO
12502                no_key = true;
12503                // The next ident should be KEY but be generous;
12504                // anything followed by UPDATE/SHARE is accepted.
12505                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12506                    if s.eq_ignore_ascii_case("key"))
12507                {
12508                    self.advance(); // KEY
12509                }
12510            }
12511            // `KEY` prefix (PG `FOR KEY SHARE`).
12512            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12513                if s.eq_ignore_ascii_case("key"))
12514            {
12515                self.advance(); // KEY
12516                key = true;
12517            }
12518            // Lock-strength keyword: UPDATE / SHARE. Required, but
12519            // we're lenient — an unexpected token here just bails
12520            // (we already consumed FOR; caller's downstream
12521            // dispatch will error if anything actually depends on
12522            // the trailing tokens).
12523            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12524                if s.eq_ignore_ascii_case("update"));
12525            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12526                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12527            {
12528                self.advance();
12529                use crate::ast::LockStrength as LS;
12530                let strength = match (is_update, no_key, key) {
12531                    (true, true, _) => LS::NoKeyUpdate,
12532                    (true, _, _) => LS::Update,
12533                    (false, _, true) => LS::KeyShare,
12534                    (false, _, _) => LS::Share,
12535                };
12536                seen = Some(crate::ast::LockingClause {
12537                    strength,
12538                    of_tables: alloc::vec::Vec::new(),
12539                    policy: crate::ast::LockWait::Wait,
12540                });
12541            } else {
12542                // FOR by itself (or `FOR KEY` with nothing after) —
12543                // give up on the lock-clause path. We've already
12544                // advanced past FOR; further attempts to parse
12545                // here would clobber state.
12546                return seen;
12547            }
12548            // Optional `OF tbl[, tbl …]`. mailrs emits this when
12549            // joining and locking only a subset of tables.
12550            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12551                if s.eq_ignore_ascii_case("of"))
12552            {
12553                self.advance(); // OF
12554                #[allow(clippy::while_let_loop)]
12555                loop {
12556                    match self.peek() {
12557                        Token::Ident(_) | Token::QuotedIdent(_) => {
12558                            // v7.39 (round 294) — the name is CAPTURED now: PG
12559                            // validates it against the FROM clause, and an
12560                            // uncaptured list silently means "lock everything".
12561                            let mut nm = match self.advance() {
12562                                Token::Ident(n) | Token::QuotedIdent(n) => n,
12563                                _ => alloc::string::String::new(),
12564                            };
12565                            // Optional schema-qualified `schema.table`.
12566                            if matches!(self.peek(), Token::Dot) {
12567                                self.advance();
12568                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12569                                {
12570                                    self.advance();
12571                                    nm = n;
12572                                }
12573                            }
12574                            if let Some(c) = seen.as_mut() {
12575                                c.of_tables.push(nm);
12576                            }
12577                        }
12578                        _ => break,
12579                    }
12580                    if matches!(self.peek(), Token::Comma) {
12581                        self.advance();
12582                    } else {
12583                        break;
12584                    }
12585                }
12586            }
12587            // Optional `NOWAIT` | `SKIP LOCKED`.
12588            match self.peek().clone() {
12589                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12590                    self.advance();
12591                    if let Some(c) = seen.as_mut() {
12592                        c.policy = crate::ast::LockWait::NoWait;
12593                    }
12594                }
12595                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12596                    self.advance(); // SKIP
12597                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12598                        if s.eq_ignore_ascii_case("locked"))
12599                    {
12600                        self.advance(); // LOCKED
12601                        if let Some(c) = seen.as_mut() {
12602                            c.policy = crate::ast::LockWait::SkipLocked;
12603                        }
12604                    }
12605                }
12606                _ => {}
12607            }
12608            // Loop: PG allows multiple FOR clauses chained.
12609        }
12610        seen
12611    }
12612
12613    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12614    /// Bind value gets resolved during prepared-statement Execute;
12615    /// the Pratt expression parser would over-accept here (e.g.
12616    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12617    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12618    /// sentinel tokens (PG synonyms for "no limit"). Returns true
12619    /// when one was consumed; caller skips the regular
12620    /// limit-value parse and leaves `head.limit` at None.
12621    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12622        if matches!(self.peek(), Token::Null) {
12623            self.advance();
12624            return true;
12625        }
12626        if matches!(self.peek(), Token::All) {
12627            self.advance();
12628            return true;
12629        }
12630        false
12631    }
12632
12633    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12634    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12635    /// SQL-standard shape. No-op when missing.
12636    fn consume_optional_rows_keyword(&mut self) {
12637        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12638            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12639        {
12640            self.advance();
12641        }
12642    }
12643
12644    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12645    ///
12646    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12647    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12648    /// constant, which is why that spelling keeps the token path below.
12649    ///
12650    /// Constants are folded here rather than carried into the tree: the
12651    /// 15+ execution paths that read the row count go through
12652    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12653    /// means "no limit". A clause the engine could not resolve would
12654    /// therefore return the WHOLE table instead of failing. Folding at
12655    /// parse time keeps that impossible; a non-constant clause is still
12656    /// a clean error (recorded residual — closing it wants a resolution
12657    /// pre-pass on the simple-query path, where `substitute_placeholders`
12658    /// does not run).
12659    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12660        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12661        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12662        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12663        // ONLY` both work (its grammar takes a c_expr). Both measured
12664        // against PG 18.4 in round 305.
12665        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12666            return self.parse_limit_constant(label);
12667        }
12668        // One pass, no rewind: `advance()` takes each token by
12669        // `mem::replace`, so a consumed token reads back as Eof and this
12670        // parser cannot backtrack. Everything — bare literal included —
12671        // is therefore folded from the parsed expression rather than
12672        // re-read from the token stream.
12673        let start = self.pos;
12674        let e = self.parse_expr(0)?;
12675        if let crate::ast::Expr::Placeholder(n) = e {
12676            return Ok(crate::ast::LimitExpr::Placeholder(n));
12677        }
12678        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12679        match fold_limit_constant(&e) {
12680            Some(Ok(v)) if v < 0 => Err(ParseError {
12681                message: alloc::format!("{neg_label} must not be negative"),
12682                token_pos: start,
12683            }),
12684            Some(Ok(v)) => u32::try_from(v)
12685                .map(crate::ast::LimitExpr::Literal)
12686                .map_err(|_| ParseError {
12687                    message: alloc::format!("{label} value too large: {v}"),
12688                    token_pos: start,
12689                }),
12690            Some(Err(message)) => Err(ParseError {
12691                message: message.replace("{L}", neg_label),
12692                token_pos: start,
12693            }),
12694            // v7.39 (round 305, V23) — not foldable at parse time
12695            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12696            // expression; the engine evaluates it once before dispatch.
12697            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12698        }
12699    }
12700
12701    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12702        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12703        // coercion rules, not just an integer token: a NUMERIC rounds half
12704        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12705        // refused with PG's wording ("LIMIT must not be negative", 2201W /
12706        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12707        // content, failing as an input-syntax error on the value. General
12708        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12709        // they need an Expr-carrying LimitExpr variant.
12710        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12711        let err_at = |message: alloc::string::String, pos: usize| ParseError {
12712            message,
12713            token_pos: pos,
12714        };
12715        match self.advance() {
12716            Token::Integer(n) if n >= 0 => u32::try_from(n)
12717                .map(crate::ast::LimitExpr::Literal)
12718                .map_err(|_| ParseError {
12719                    message: alloc::format!("{label} value too large: {n}"),
12720                    token_pos: self.consumed_pos(),
12721                }),
12722            Token::Integer(_) => Err(err_at(
12723                alloc::format!("{neg_label} must not be negative"),
12724                self.pos.saturating_sub(1),
12725            )),
12726            Token::Numeric(t) => {
12727                let pos = self.pos.saturating_sub(1);
12728                let v: f64 = t.parse().map_err(|_| {
12729                    err_at(
12730                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12731                        pos,
12732                    )
12733                })?;
12734                if v < 0.0 {
12735                    return Err(err_at(
12736                        alloc::format!("{neg_label} must not be negative"),
12737                        pos,
12738                    ));
12739                }
12740                // Round half away from zero — PG's numeric→bigint cast.
12741                // (no_std: no f64::round; v is non-negative, so truncating
12742                // v + 0.5 is the same thing.)
12743                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12744                let rounded = (v + 0.5) as u64;
12745                u32::try_from(rounded)
12746                    .map(crate::ast::LimitExpr::Literal)
12747                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12748            }
12749            Token::Minus => {
12750                let pos = self.pos.saturating_sub(1);
12751                match self.peek() {
12752                    Token::Integer(_) | Token::Numeric(_) => {
12753                        self.advance();
12754                        Err(err_at(
12755                            alloc::format!("{neg_label} must not be negative"),
12756                            pos,
12757                        ))
12758                    }
12759                    other => Err(err_at(
12760                        alloc::format!(
12761                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12762                        ),
12763                        pos,
12764                    )),
12765                }
12766            }
12767            Token::String(t) => {
12768                let pos = self.pos.saturating_sub(1);
12769                match t.trim().parse::<i64>() {
12770                    Ok(n) if n < 0 => Err(err_at(
12771                        alloc::format!("{neg_label} must not be negative"),
12772                        pos,
12773                    )),
12774                    Ok(n) => u32::try_from(n)
12775                        .map(crate::ast::LimitExpr::Literal)
12776                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12777                    Err(_) => Err(err_at(
12778                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12779                        pos,
12780                    )),
12781                }
12782            }
12783            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12784            other => Err(ParseError {
12785                message: alloc::format!(
12786                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12787                ),
12788                token_pos: self.consumed_pos(),
12789            }),
12790        }
12791    }
12792
12793    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12794    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12795    /// `unions` empty and `order_by` / `limit` `None`; the top-level
12796    /// `parse_select_stmt` is responsible for filling those in.
12797    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12798    /// call in the expression tree to the per-set integer bitmask
12799    /// (PG semantics: one bit per argument, MSB first; 1 = the key
12800    /// is dropped in this grouping set). Runs during the ROLLUP /
12801    /// CUBE / GROUPING SETS expansion, where the set is known.
12802    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12803    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12804    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12805        if let Expr::FunctionCall { name, .. } = expr
12806            && name.eq_ignore_ascii_case("grouping")
12807        {
12808            if !out.iter().any(|e| e == expr) {
12809                out.push(expr.clone());
12810            }
12811            return;
12812        }
12813        match expr {
12814            Expr::Binary { lhs, rhs, .. } => {
12815                Self::collect_grouping_calls(lhs, out);
12816                Self::collect_grouping_calls(rhs, out);
12817            }
12818            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12819                Self::collect_grouping_calls(expr, out)
12820            }
12821            Expr::FunctionCall { args, .. } => {
12822                for a in args {
12823                    Self::collect_grouping_calls(a, out);
12824                }
12825            }
12826            Expr::Case {
12827                operand,
12828                branches,
12829                else_branch,
12830            } => {
12831                if let Some(o) = operand {
12832                    Self::collect_grouping_calls(o, out);
12833                }
12834                for (c, v) in branches {
12835                    Self::collect_grouping_calls(c, out);
12836                    Self::collect_grouping_calls(v, out);
12837                }
12838                if let Some(x) = else_branch {
12839                    Self::collect_grouping_calls(x, out);
12840                }
12841            }
12842            _ => {}
12843        }
12844    }
12845
12846    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12847    /// `grp_exprs[k]` with a reference to the synthetic ordering column
12848    /// `__grp_ord_k` (injected per grouping-set branch).
12849    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12850        if let Expr::FunctionCall { name, .. } = expr
12851            && name.eq_ignore_ascii_case("grouping")
12852        {
12853            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
12854                *expr = Expr::Column(crate::ast::ColumnName {
12855                    qualifier: None,
12856                    name: alloc::format!("__grp_ord_{k}"),
12857                });
12858            }
12859            return;
12860        }
12861        match expr {
12862            Expr::Binary { lhs, rhs, .. } => {
12863                Self::rewrite_grouping_to_col(lhs, grp_exprs);
12864                Self::rewrite_grouping_to_col(rhs, grp_exprs);
12865            }
12866            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12867                Self::rewrite_grouping_to_col(expr, grp_exprs)
12868            }
12869            Expr::FunctionCall { args, .. } => {
12870                for a in args {
12871                    Self::rewrite_grouping_to_col(a, grp_exprs);
12872                }
12873            }
12874            Expr::Case {
12875                operand,
12876                branches,
12877                else_branch,
12878            } => {
12879                if let Some(o) = operand {
12880                    Self::rewrite_grouping_to_col(o, grp_exprs);
12881                }
12882                for (c, v) in branches {
12883                    Self::rewrite_grouping_to_col(c, grp_exprs);
12884                    Self::rewrite_grouping_to_col(v, grp_exprs);
12885                }
12886                if let Some(x) = else_branch {
12887                    Self::rewrite_grouping_to_col(x, grp_exprs);
12888                }
12889            }
12890            _ => {}
12891        }
12892    }
12893
12894    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
12895    /// as the list of key sets it contributes. A bare expression is one
12896    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
12897    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
12898    /// the concatenation of its items' sets, where an item is itself an
12899    /// element, a parenthesized key list, or the empty set `()`. A
12900    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
12901    /// move together.
12902    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
12903        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
12904        // ROLLUP ( … ) / CUBE ( … )
12905        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
12906            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
12907        {
12908            let is_cube = is_kw(self.peek(), "cube");
12909            self.advance(); // ROLLUP / CUBE
12910            self.advance(); // (
12911            let mut units: Vec<Vec<Expr>> = Vec::new();
12912            loop {
12913                if matches!(self.peek(), Token::LParen) {
12914                    // Composite unit: (a, b) rolls up as one.
12915                    self.advance();
12916                    let mut unit = Vec::new();
12917                    if !matches!(self.peek(), Token::RParen) {
12918                        loop {
12919                            unit.push(self.parse_expr(0)?);
12920                            match self.peek() {
12921                                Token::Comma => {
12922                                    self.advance();
12923                                }
12924                                Token::RParen => break,
12925                                other => {
12926                                    return Err(self.err(format!(
12927                                        "expected ',' or ')' in grouping unit, got {other:?}"
12928                                    )));
12929                                }
12930                            }
12931                        }
12932                    }
12933                    self.advance(); // )
12934                    units.push(unit);
12935                } else {
12936                    units.push(alloc::vec![self.parse_expr(0)?]);
12937                }
12938                match self.peek() {
12939                    Token::Comma => {
12940                        self.advance();
12941                    }
12942                    Token::RParen => break,
12943                    other => {
12944                        return Err(self.err(format!(
12945                            "expected ',' or ')' in grouping list, got {other:?}"
12946                        )));
12947                    }
12948                }
12949            }
12950            self.advance(); // )
12951            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
12952                units
12953                    .iter()
12954                    .zip(unit_sel.iter())
12955                    .filter(|(_, keep)| **keep)
12956                    .flat_map(|(u, _)| u.iter().cloned())
12957                    .collect()
12958            };
12959            let n = units.len();
12960            if is_cube {
12961                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
12962                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
12963                    .collect();
12964                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
12965                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
12966            }
12967            return Ok((0..=n)
12968                .rev()
12969                .map(|keep| {
12970                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
12971                    flatten(&sel)
12972                })
12973                .collect());
12974        }
12975        // GROUPING SETS ( item [, item]* )
12976        if is_kw(self.peek(), "grouping")
12977            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
12978        {
12979            self.advance(); // GROUPING
12980            self.advance(); // SETS
12981            if !matches!(self.peek(), Token::LParen) {
12982                return Err(self.err(format!(
12983                    "expected '(' after GROUPING SETS, got {:?}",
12984                    self.peek()
12985                )));
12986            }
12987            self.advance(); // outer (
12988            let mut sets: Vec<Vec<Expr>> = Vec::new();
12989            loop {
12990                if matches!(self.peek(), Token::LParen) {
12991                    // A parenthesized key list (or the empty set).
12992                    self.advance();
12993                    let mut set = Vec::new();
12994                    if !matches!(self.peek(), Token::RParen) {
12995                        loop {
12996                            set.push(self.parse_expr(0)?);
12997                            match self.peek() {
12998                                Token::Comma => {
12999                                    self.advance();
13000                                }
13001                                Token::RParen => break,
13002                                other => {
13003                                    return Err(self.err(format!(
13004                                        "expected ',' or ')' in grouping set, got {other:?}"
13005                                    )));
13006                                }
13007                            }
13008                        }
13009                    }
13010                    self.advance(); // )
13011                    sets.push(set);
13012                } else {
13013                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13014                    // bare expression.
13015                    sets.extend(self.parse_grouping_element()?);
13016                }
13017                match self.peek() {
13018                    Token::Comma => {
13019                        self.advance();
13020                    }
13021                    Token::RParen => break,
13022                    other => {
13023                        return Err(self.err(format!(
13024                            "expected ',' or ')' after a grouping set, got {other:?}"
13025                        )));
13026                    }
13027                }
13028            }
13029            self.advance(); // outer )
13030            return Ok(sets);
13031        }
13032        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13033    }
13034
13035    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13036        // v7.38 (read01) — a reference to a key that is dropped in this grouping
13037        // set evaluates to NULL, at any depth. Previously only a *top-level*
13038        // select item equal to a dropped key was nullified, so a key nested in
13039        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13040        // column and failed to resolve against the set's synthetic schema.
13041        if dropped.iter().any(|d| d == expr) {
13042            *expr = Expr::Literal(Literal::Null);
13043            return;
13044        }
13045        if let Expr::FunctionCall { name, args } = expr
13046            && name.eq_ignore_ascii_case("grouping")
13047        {
13048            let mut mask: i64 = 0;
13049            for a in args.iter() {
13050                mask <<= 1;
13051                if dropped.iter().any(|d| d == a) {
13052                    mask |= 1;
13053                }
13054            }
13055            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13056            // literal: a bare integer in a select item is indistinguishable
13057            // from a positional reference once `ORDER BY 1` substitutes the
13058            // item back in, and the round-232 position check then read the
13059            // mask value as an out-of-range position. The cast changes
13060            // nothing semantically (grouping() is integer).
13061            *expr = Expr::Cast {
13062                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13063                target: crate::ast::CastTarget::Int,
13064            };
13065            return;
13066        }
13067        // Generic recursion over the common expression shapes the
13068        // SELECT list uses; anything without child expressions is
13069        // left alone.
13070        match expr {
13071            Expr::FunctionCall { args, .. } => {
13072                for a in args {
13073                    Self::substitute_grouping_calls(a, dropped);
13074                }
13075            }
13076            Expr::Binary { lhs, rhs, .. } => {
13077                Self::substitute_grouping_calls(lhs, dropped);
13078                Self::substitute_grouping_calls(rhs, dropped);
13079            }
13080            Expr::Unary { expr: inner, .. } => {
13081                Self::substitute_grouping_calls(inner, dropped);
13082            }
13083            Expr::Cast { expr: inner, .. } => {
13084                Self::substitute_grouping_calls(inner, dropped);
13085            }
13086            Expr::Case {
13087                operand,
13088                branches,
13089                else_branch,
13090            } => {
13091                if let Some(op) = operand {
13092                    Self::substitute_grouping_calls(op, dropped);
13093                }
13094                for (w, t) in branches {
13095                    Self::substitute_grouping_calls(w, dropped);
13096                    Self::substitute_grouping_calls(t, dropped);
13097                }
13098                if let Some(e) = else_branch {
13099                    Self::substitute_grouping_calls(e, dropped);
13100                }
13101            }
13102            // v7.38 (read01) — recurse into the remaining child-bearing shapes
13103            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13104            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13105            // …` is the canonical rollup-total label idiom).
13106            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13107            Expr::Like { expr, pattern, .. } => {
13108                Self::substitute_grouping_calls(expr, dropped);
13109                Self::substitute_grouping_calls(pattern, dropped);
13110            }
13111            Expr::InList { expr, list, .. } => {
13112                Self::substitute_grouping_calls(expr, dropped);
13113                for item in list {
13114                    Self::substitute_grouping_calls(item, dropped);
13115                }
13116            }
13117            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13118            Expr::Array(items) => {
13119                for item in items {
13120                    Self::substitute_grouping_calls(item, dropped);
13121                }
13122            }
13123            Expr::ArraySubscript { target, index } => {
13124                Self::substitute_grouping_calls(target, dropped);
13125                Self::substitute_grouping_calls(index, dropped);
13126            }
13127            Expr::ArraySlice { target, lo, hi } => {
13128                Self::substitute_grouping_calls(target, dropped);
13129                if let Some(lo) = lo {
13130                    Self::substitute_grouping_calls(lo, dropped);
13131                }
13132                if let Some(hi) = hi {
13133                    Self::substitute_grouping_calls(hi, dropped);
13134                }
13135            }
13136            Expr::AnyAll { expr, array, .. } => {
13137                Self::substitute_grouping_calls(expr, dropped);
13138                Self::substitute_grouping_calls(array, dropped);
13139            }
13140            _ => {}
13141        }
13142    }
13143
13144    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13145        // v7.37.17 (17.6 siblings) — parenthesized set-operation
13146        // group: `( <select chain> )` usable anywhere a query block
13147        // is (head or peer of an outer chain). The group's own
13148        // unions ride the returned SelectStatement; the executor's
13149        // nested-peer recursion runs them.
13150        if matches!(self.peek(), Token::LParen)
13151            && matches!(
13152                self.tokens.get(self.pos + 1),
13153                Some(Token::Select | Token::LParen | Token::Values)
13154            )
13155        {
13156            self.advance(); // (
13157            self.enter_nested()?;
13158            // v7.37 D.20 — a group whose head is a VALUES list:
13159            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13160            // otherwise recurse into a nested SELECT/group head.
13161            let mut head = (if matches!(self.peek(), Token::Values) {
13162                self.advance(); // VALUES
13163                self.parse_values_rows_body()
13164            } else {
13165                self.parse_bare_select()
13166            })
13167            .and_then(|mut h| {
13168                self.parse_setop_chain_into(&mut h)?;
13169                Ok(h)
13170            });
13171            self.nest_depth -= 1;
13172            let mut head = match &mut head {
13173                Ok(h) => core::mem::take(h),
13174                Err(_) => return head,
13175            };
13176            // v7.37.17 (17.6 siblings) — group-internal tail:
13177            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13178            // group head, then wrap the group as a derived table
13179            // (SELECT * FROM (group)) so the outer chain / outer
13180            // tail can't clobber the group's own ordering or limit.
13181            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13182                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13183                    if s.eq_ignore_ascii_case("fetch"));
13184            if has_tail {
13185                self.parse_select_tail_into(&mut head)?;
13186                head = SelectStatement {
13187                    locking: None,
13188                    ctes: Vec::new(),
13189                    distinct: false,
13190                    distinct_on: Vec::new(),
13191                    items: alloc::vec![SelectItem::Wildcard],
13192                    from: Some(FromClause {
13193                        primary: TableRef {
13194                            name: "subquery".to_string(),
13195                            alias: None,
13196                            only: false,
13197                            as_of_segment: None,
13198                            unnest_expr: None,
13199                            unnest_column_aliases: Vec::new(),
13200                            with_ordinality: false,
13201                            generate_series_args: None,
13202                            lateral_subquery: Some(Box::new(head)),
13203                            jsonb_each_text_arg: None,
13204                            table_fn_call: None,
13205                            rows_from: None,
13206                            json_table: None,
13207                            scalar_fn_item: false,
13208                        },
13209                        joins: Vec::new(),
13210                    }),
13211                    where_: None,
13212                    group_by: None,
13213                    group_by_all: false,
13214                    having: None,
13215                    unions: Vec::new(),
13216                    order_by: Vec::new(),
13217                    limit: None,
13218                    offset: None,
13219                    limit_with_ties: false,
13220                    window_check_exprs: Vec::new(),
13221                };
13222            }
13223            if !matches!(self.peek(), Token::RParen) {
13224                return Err(self.err(format!(
13225                    "expected ')' after parenthesized query group, got {:?}",
13226                    self.peek()
13227                )));
13228            }
13229            self.advance();
13230            return Ok(head);
13231        }
13232        // `TABLE name` shorthand as a query block — valid anywhere
13233        // a SELECT head is (set-op peers included).
13234        if matches!(self.peek(), Token::Table)
13235            && matches!(
13236                self.tokens.get(self.pos + 1),
13237                Some(Token::Ident(_) | Token::QuotedIdent(_))
13238            )
13239        {
13240            return self.parse_table_shorthand();
13241        }
13242        if !matches!(self.peek(), Token::Select) {
13243            return Err(self.err(format!(
13244                "expected SELECT to start a query block, got {:?}",
13245                self.peek()
13246            )));
13247        }
13248        self.advance();
13249        let distinct = if matches!(self.peek(), Token::Distinct) {
13250            self.advance();
13251            true
13252        } else {
13253            false
13254        };
13255        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13256        // keep the first row (per ORDER BY) of each group the
13257        // expressions define. Django's .distinct('field') shape.
13258        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13259            self.advance(); // ON
13260            if !matches!(self.peek(), Token::LParen) {
13261                return Err(self.err(format!(
13262                    "expected '(' after DISTINCT ON, got {:?}",
13263                    self.peek()
13264                )));
13265            }
13266            self.advance();
13267            let mut exprs = Vec::new();
13268            loop {
13269                exprs.push(self.parse_expr(0)?);
13270                match self.peek() {
13271                    Token::Comma => {
13272                        self.advance();
13273                    }
13274                    Token::RParen => break,
13275                    other => {
13276                        return Err(self.err(format!(
13277                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13278                        )));
13279                    }
13280                }
13281            }
13282            self.advance(); // )
13283            exprs
13284        } else {
13285            Vec::new()
13286        };
13287        let mut items = self.parse_select_list()?;
13288        // Scope the TABLESAMPLE lowering channel to this SELECT:
13289        // stash whatever an enclosing select accumulated, collect
13290        // our own FROM's predicates, restore after the combine.
13291        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13292        let mut from = if matches!(self.peek(), Token::From) {
13293            self.advance();
13294            Some(self.parse_from_clause()?)
13295        } else {
13296            None
13297        };
13298        // v7.37 D.22 — a set-returning function in the projection with no FROM
13299        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13300        // rows. Move the first SRF projection item to a FROM-position derived
13301        // table and replace it in the projection with a reference to its output
13302        // column; sibling scalar columns repeat per SRF row. PG names the output
13303        // column after the function (or its AS alias). Reuses the FROM-SRF
13304        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13305        // works via the targetlist-SRF path.
13306        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13307        // `SELECT * FROM f(args)` — the record's fields become the columns, which
13308        // is exactly what the function's own row shape already is. Anywhere else
13309        // (per outer row, or beside other items) it would need a real record-typed
13310        // projection, so it says so rather than answering something else.
13311        if let [
13312            SelectItem::Expr {
13313                expr: Expr::FunctionCall { name, args },
13314                ..
13315            },
13316        ] = items.as_slice()
13317            && name == "__record_expand"
13318        {
13319            let Some(Expr::FunctionCall {
13320                name: inner_name,
13321                args: inner_args,
13322            }) = args.first()
13323            else {
13324                return Err(self.err(
13325                    "(<expr>).* expands a function's record — it needs a function call".into(),
13326                ));
13327            };
13328            if from.is_some() {
13329                return Err(self.err(
13330                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13331                        .into(),
13332                ));
13333            }
13334            let fn_ref = TableRef {
13335                name: inner_name.clone(),
13336                alias: None,
13337                only: false,
13338                as_of_segment: None,
13339                unnest_expr: None,
13340                unnest_column_aliases: Vec::new(),
13341                with_ordinality: false,
13342                generate_series_args: None,
13343                lateral_subquery: None,
13344                jsonb_each_text_arg: None,
13345                table_fn_call: Some(Box::new((
13346                    inner_name.to_ascii_lowercase(),
13347                    inner_args.clone(),
13348                ))),
13349                rows_from: None,
13350                json_table: None,
13351                scalar_fn_item: false,
13352            };
13353            items = alloc::vec![SelectItem::Wildcard];
13354            from = Some(FromClause {
13355                primary: fn_ref,
13356                joins: Vec::new(),
13357            });
13358        }
13359        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13360        // FROM, keeps its marker: the ENGINE lowers it, because naming the
13361        // record's fields takes the catalog. It becomes a LATERAL of the same
13362        // function plus one item per declared column — the machinery rounds 65
13363        // and 69 already built.
13364        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13365        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13366        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13367        // express, since the lifted one becomes a scan and the other would
13368        // expand per its rows (a cross product, not a zip). So when the
13369        // projection holds more than one top-level function call, the lift steps
13370        // aside and the engine's target-list expansion takes the whole list.
13371        let fn_call_items = items
13372            .iter()
13373            .filter(|it| {
13374                matches!(
13375                    it,
13376                    SelectItem::Expr {
13377                        expr: Expr::FunctionCall { .. },
13378                        ..
13379                    }
13380                )
13381            })
13382            .count();
13383        if from.is_none() && fn_call_items <= 1 {
13384            let mut found: Option<(usize, TableRef, String)> = None;
13385            for (i, item) in items.iter().enumerate() {
13386                if let SelectItem::Expr {
13387                    expr: Expr::FunctionCall { name, args },
13388                    alias,
13389                } = item
13390                {
13391                    let lname = name.to_ascii_lowercase();
13392                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
13393                    let (unnest, gs) = match lname.as_str() {
13394                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13395                        "generate_series" if (2..=3).contains(&args.len()) => {
13396                            (None, Some(args.clone()))
13397                        }
13398                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
13399                        // no-FROM projection yields the 1-based subscripts, i.e.
13400                        // generate_series(1, array_length(arr, dim)); an invalid
13401                        // dimension makes array_length NULL → 0 rows, as in PG.
13402                        "generate_subscripts" if args.len() == 2 => (
13403                            None,
13404                            Some(alloc::vec![
13405                                Expr::Literal(Literal::Integer(1)),
13406                                Expr::FunctionCall {
13407                                    name: "array_length".to_string(),
13408                                    args: args.clone(),
13409                                },
13410                            ]),
13411                        ),
13412                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13413                        // in a no-FROM projection unnest their *_to_array form.
13414                        "string_to_table" | "regexp_split_to_table" => {
13415                            let array_fn = if lname == "string_to_table" {
13416                                "string_to_array"
13417                            } else {
13418                                "regexp_split_to_array"
13419                            };
13420                            (
13421                                Some(Box::new(Expr::FunctionCall {
13422                                    name: array_fn.to_string(),
13423                                    args: args.clone(),
13424                                })),
13425                                None,
13426                            )
13427                        }
13428                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13429                        // a no-FROM projection expand per element. The scalar form
13430                        // returns the elements as a TEXT array, so unnest over the
13431                        // same call materialises one row each (same rewrite the
13432                        // FROM-clause form uses).
13433                        "jsonb_array_elements"
13434                        | "json_array_elements"
13435                        | "jsonb_array_elements_text"
13436                        | "json_array_elements_text"
13437                            if args.len() == 1 =>
13438                        {
13439                            (
13440                                Some(Box::new(Expr::FunctionCall {
13441                                    name: lname.clone(),
13442                                    args: args.clone(),
13443                                })),
13444                                None,
13445                            )
13446                        }
13447                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13448                        // in a no-FROM projection expands per match (scalar form
13449                        // returns the matches as a TEXT array → unnest).
13450                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13451                            Some(Box::new(Expr::FunctionCall {
13452                                name: lname.clone(),
13453                                args: args.clone(),
13454                            })),
13455                            None,
13456                        ),
13457                        _ => continue,
13458                    };
13459                    found = Some((
13460                        i,
13461                        TableRef {
13462                            name: colname.clone(),
13463                            alias: Some(colname.clone()),
13464                            only: false,
13465                            as_of_segment: None,
13466                            unnest_expr: unnest,
13467                            unnest_column_aliases: alloc::vec![colname.clone()],
13468                            with_ordinality: false,
13469                            generate_series_args: gs,
13470                            lateral_subquery: None,
13471                            jsonb_each_text_arg: None,
13472                            table_fn_call: None,
13473                            rows_from: None,
13474                            json_table: None,
13475                            scalar_fn_item: false,
13476                        },
13477                        colname,
13478                    ));
13479                    break;
13480                }
13481            }
13482            if let Some((idx, tref, colname)) = found {
13483                from = Some(FromClause {
13484                    primary: tref,
13485                    joins: Vec::new(),
13486                });
13487                items[idx] = SelectItem::Expr {
13488                    expr: Expr::Column(ColumnName {
13489                        qualifier: None,
13490                        name: colname.clone(),
13491                    }),
13492                    alias: Some(colname),
13493                };
13494            }
13495        }
13496        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13497        let where_ = if matches!(self.peek(), Token::Where) {
13498            self.advance();
13499            Some(self.parse_expr(0)?)
13500        } else {
13501            None
13502        };
13503        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13504            Some(match acc {
13505                Some(w) => Expr::Binary {
13506                    lhs: Box::new(pred),
13507                    op: crate::ast::BinOp::And,
13508                    rhs: Box::new(w),
13509                },
13510                None => pred,
13511            })
13512        });
13513        self.pending_sample_preds = enclosing_sample_preds;
13514        let mut group_by_all = false;
13515        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13516        // share one expansion: `grouping_sets` lists the key subsets
13517        // (first = primary, assigned to stmt.group_by; the rest
13518        // become UNION ALL peers), `grouping_universe` is the full
13519        // key list used to compute each peer's dropped keys.
13520        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13521        let mut grouping_universe: Vec<Expr> = Vec::new();
13522        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13523        // A BOOL, not the key list: this frame is the statement parser's, and
13524        // round 430 measured that a `Vec` local here is enough on its own to
13525        // tip the 512 KiB nesting guard. The keys are recoverable from
13526        // `grouping_universe`, which a rollup fills with exactly them.
13527        let mut mysql_rollup = false;
13528        let group_by = if matches!(self.peek(), Token::Group) {
13529            self.advance();
13530            if !self.peek_is_by() {
13531                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13532            }
13533            self.advance();
13534            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13535            // every non-aggregate SELECT-list item later.
13536            if matches!(self.peek(), Token::All) {
13537                self.advance();
13538                group_by_all = true;
13539                None
13540            } else {
13541                // v7.39 (round 242) — PG's general grouping-element grammar:
13542                // GROUP BY [DISTINCT] element [, element]*, where an element
13543                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13544                // SETS (…) — mixed freely. Each element yields a list of
13545                // key sets; the query's grouping sets are the CARTESIAN
13546                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13547                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13548                // content. ROLLUP/CUBE members may be composite
13549                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13550                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13551                // parser handled only a lone ROLLUP/CUBE/GS as the whole
13552                // clause.
13553                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13554                    self.advance();
13555                    true
13556                } else {
13557                    false
13558                };
13559                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13560                loop {
13561                    element_sets.push(self.parse_grouping_element()?);
13562                    if matches!(self.peek(), Token::Comma) {
13563                        self.advance();
13564                    } else {
13565                        break;
13566                    }
13567                }
13568                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13569                for el in &element_sets {
13570                    let mut next: Vec<Vec<Expr>> = Vec::new();
13571                    for base in &total {
13572                        for set in el {
13573                            let mut merged = base.clone();
13574                            for k in set {
13575                                if !merged.iter().any(|m| m == k) {
13576                                    merged.push(k.clone());
13577                                }
13578                            }
13579                            next.push(merged);
13580                        }
13581                    }
13582                    total = next;
13583                }
13584                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13585                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13586                // The keys and the aggregates come out identical; the ROW
13587                // ORDER does not, and that is the part a report depends on.
13588                // MySQL interleaves each group's subtotal right after its
13589                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13590                // where the union-of-grouping-sets expansion emits every
13591                // leaf first and then every subtotal. MariaDB REFUSES an
13592                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13593                // order itself — measured on MariaDB 11 and MySQL 9.7, which
13594                // agree on the order and disagree only on whether ORDER BY
13595                // is allowed (MySQL allows it; SPG allows it too, since
13596                // refusing would break the clients that can write it).
13597                if self.mysql_dialect
13598                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13599                    && matches!(
13600                        self.tokens.get(self.pos + 1),
13601                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13602                    )
13603                {
13604                    self.advance(); // WITH
13605                    self.advance(); // ROLLUP
13606                    let keys = total.into_iter().next().unwrap_or_default();
13607                    mysql_rollup = true;
13608                    // n+1 prefixes, largest first — the same expansion
13609                    // `ROLLUP (…)` produces.
13610                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13611                }
13612                if distinct_sets {
13613                    let mut seen: Vec<Vec<String>> = Vec::new();
13614                    total.retain(|set| {
13615                        let mut key: Vec<String> =
13616                            set.iter().map(|e| alloc::format!("{e}")).collect();
13617                        key.sort();
13618                        if seen.contains(&key) {
13619                            false
13620                        } else {
13621                            seen.push(key);
13622                            true
13623                        }
13624                    });
13625                }
13626                if total.len() > 1 {
13627                    let mut universe: Vec<Expr> = Vec::new();
13628                    for set in &total {
13629                        for k in set {
13630                            if !universe.iter().any(|u| u == k) {
13631                                universe.push(k.clone());
13632                            }
13633                        }
13634                    }
13635                    grouping_universe = universe;
13636                    let primary = total[0].clone();
13637                    grouping_sets = total;
13638                    Some(primary)
13639                } else {
13640                    // One set (a plain GROUP BY list, or a single-set
13641                    // spelling like GROUPING SETS ((a, b))). An EMPTY
13642                    // single set — GROUPING SETS (()) — stays
13643                    // `Some(vec![])`: the grand-total group, which must
13644                    // run the aggregate path.
13645                    Some(total.into_iter().next().unwrap_or_default())
13646                }
13647            }
13648        } else {
13649            None
13650        };
13651        let having = if matches!(self.peek(), Token::Having) {
13652            self.advance();
13653            Some(self.parse_expr(0)?)
13654        } else {
13655            None
13656        };
13657        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13658        // OVER w parsed to a marker above; inline each definition
13659        // into the referencing WindowFunction nodes.
13660        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13661        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13662            self.advance();
13663            loop {
13664                let wname = self.expect_ident_like()?;
13665                if !matches!(self.peek(), Token::As) {
13666                    return Err(self.err(format!(
13667                        "expected AS after WINDOW {wname}, got {:?}",
13668                        self.peek()
13669                    )));
13670                }
13671                self.advance();
13672                // v7.39 (round 229) — PG rejects a redefinition outright.
13673                if window_defs
13674                    .iter()
13675                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13676                {
13677                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13678                }
13679                let def = self.parse_over_clause()?;
13680                // A definition may itself copy an earlier one
13681                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13682                // so resolve it against the defs already in scope. Same
13683                // copy rules as an `OVER (w1 …)` in the select list.
13684                let mut probe = Expr::WindowFunction {
13685                    name: String::new(),
13686                    args: Vec::new(),
13687                    partition_by: def.0,
13688                    order_by: def.1,
13689                    frame: def.2,
13690                    null_treatment: crate::ast::NullTreatment::Respect,
13691                    filter: None,
13692                };
13693                Self::substitute_named_windows(&mut probe, &window_defs)
13694                    .map_err(|m| self.err(m))?;
13695                let Expr::WindowFunction {
13696                    partition_by,
13697                    order_by,
13698                    frame,
13699                    ..
13700                } = probe
13701                else {
13702                    unreachable!("probe is a WindowFunction")
13703                };
13704                window_defs.push((wname, (partition_by, order_by, frame)));
13705                if matches!(self.peek(), Token::Comma) {
13706                    self.advance();
13707                    continue;
13708                }
13709                break;
13710            }
13711        }
13712        // v7.39 (round 705) — which definitions did anything reference?
13713        // The ones nothing did used to be dropped here, unexamined, so
13714        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13715        // definition whether referenced or not. Their key expressions ride
13716        // out on the statement for the engine to resolve.
13717        let mut window_refs: Vec<String> = Vec::new();
13718        if !window_defs.is_empty() {
13719            for it in &items {
13720                if let SelectItem::Expr { expr, .. } = it {
13721                    Self::collect_named_window_refs(expr, &mut window_refs);
13722                }
13723            }
13724        }
13725        let window_check_exprs: Vec<Expr> = window_defs
13726            .iter()
13727            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13728            .flat_map(|(_, (partition, order, _))| {
13729                partition
13730                    .iter()
13731                    .cloned()
13732                    .chain(order.iter().map(|(e, _, _)| e.clone()))
13733            })
13734            .collect();
13735        if !window_defs.is_empty()
13736            || items
13737                .iter()
13738                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13739        {
13740            for it in &mut items {
13741                if let SelectItem::Expr { expr, .. } = it {
13742                    Self::substitute_named_windows(expr, &window_defs)
13743                        .map_err(|m| self.err(m))?;
13744                }
13745            }
13746        }
13747        // `GROUP BY 1` — positional keys substitute with the Nth
13748        // select item's expression (same contract ORDER BY has had
13749        // since v6.x). Out-of-range positions error.
13750        let group_by = match group_by {
13751            Some(mut keys) => {
13752                for k in &mut keys {
13753                    if let Expr::Literal(Literal::Integer(n)) = k {
13754                        let idx = *n;
13755                        if idx < 1 || idx as usize > items.len() {
13756                            return Err(self.err(alloc::format!(
13757                                "GROUP BY position {idx} is not in select list"
13758                            )));
13759                        }
13760                        match &items[(idx - 1) as usize] {
13761                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
13762                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13763                                return Err(self.err(alloc::format!(
13764                                    "GROUP BY position {idx} references a wildcard item"
13765                                )));
13766                            }
13767                        }
13768                    }
13769                }
13770                Some(keys)
13771            }
13772            None => None,
13773        };
13774        let mut stmt = SelectStatement {
13775            locking: None,
13776            ctes: Vec::new(),
13777            distinct,
13778            distinct_on,
13779            items,
13780            from,
13781            where_,
13782            group_by,
13783            group_by_all,
13784            having,
13785            unions: Vec::new(),
13786            order_by: Vec::new(),
13787            limit: None,
13788            offset: None,
13789            limit_with_ties: false,
13790            window_check_exprs,
13791        };
13792        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13793        // first set is the primary (already on stmt.group_by); each
13794        // further set becomes a UNION ALL peer with its dropped
13795        // keys (universe minus the set) replaced by NULL literals
13796        // in the peer's items and group_by. PG-legal: non-grouped
13797        // select items must be group keys or aggregates, so a
13798        // dropped key's occurrences in the projection are exactly
13799        // the ones to nullify.
13800        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
13801        // over a plain GROUP BY (every argument must be a group key; the
13802        // mask is then 0) and rejects anything else with 42803. SPG's
13803        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
13804        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
13805        // function `grouping`".
13806        if grouping_sets.len() <= 1 {
13807            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
13808            let mut calls: Vec<Expr> = Vec::new();
13809            for item in &stmt.items {
13810                if let SelectItem::Expr { expr, .. } = item {
13811                    Self::collect_grouping_calls(expr, &mut calls);
13812                }
13813            }
13814            if let Some(h) = &stmt.having {
13815                Self::collect_grouping_calls(h, &mut calls);
13816            }
13817            for call in &calls {
13818                let Expr::FunctionCall { args, .. } = call else {
13819                    continue;
13820                };
13821                for a in args {
13822                    if !keys.iter().any(|k| k == a) {
13823                        return Err(self.err(
13824                            "arguments to GROUPING must be grouping expressions of the associated query level"
13825                                .to_string(),
13826                        ));
13827                    }
13828                }
13829            }
13830            if !calls.is_empty() {
13831                for item in &mut stmt.items {
13832                    if let SelectItem::Expr { expr, .. } = item {
13833                        Self::substitute_grouping_calls(expr, &[]);
13834                    }
13835                }
13836                if let Some(h) = &mut stmt.having {
13837                    Self::substitute_grouping_calls(h, &[]);
13838                }
13839            }
13840        }
13841        if grouping_sets.len() > 1 {
13842            // The primary set's own dropped keys nullify in the
13843            // HEAD's projection too (GROUPING SETS's first set may
13844            // omit keys other sets use).
13845            let primary = grouping_sets[0].clone();
13846            let head_dropped: Vec<Expr> = grouping_universe
13847                .iter()
13848                .filter(|u| !primary.iter().any(|k| k == *u))
13849                .cloned()
13850                .collect();
13851            for set in grouping_sets.iter().skip(1) {
13852                let mut peer = stmt.clone();
13853                peer.unions = Vec::new();
13854                let dropped: Vec<&Expr> = grouping_universe
13855                    .iter()
13856                    .filter(|u| !set.iter().any(|k| k == *u))
13857                    .collect();
13858                // Empty set = grand-total group: `Some(vec![])` forces
13859                // the aggregate path (one collapsed row) instead of a
13860                // per-row passthrough. See the primary-set note above.
13861                peer.group_by = Some(set.clone());
13862                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
13863                for item in &mut peer.items {
13864                    if let SelectItem::Expr { expr, alias } = item {
13865                        if dropped.iter().any(|d| *d == expr) {
13866                            // v7.39 — keep the dropped key's name on the
13867                            // NULL literal so the UNION output column
13868                            // (and any top-level ORDER BY on it) still
13869                            // resolves.
13870                            if alias.is_none()
13871                                && let Expr::Column(c) = &expr
13872                            {
13873                                *alias = Some(c.name.clone());
13874                            }
13875                            *expr = Expr::Literal(Literal::Null);
13876                        } else {
13877                            Self::substitute_grouping_calls(expr, &dropped_owned);
13878                        }
13879                    }
13880                }
13881                if let Some(h) = &mut peer.having {
13882                    Self::substitute_grouping_calls(h, &dropped_owned);
13883                }
13884                stmt.unions.push((UnionKind::All, peer));
13885            }
13886            for item in &mut stmt.items {
13887                if let SelectItem::Expr { expr, alias } = item {
13888                    if head_dropped.iter().any(|d| d == expr) {
13889                        if alias.is_none()
13890                            && let Expr::Column(c) = &expr
13891                        {
13892                            *alias = Some(c.name.clone());
13893                        }
13894                        *expr = Expr::Literal(Literal::Null);
13895                    } else {
13896                        Self::substitute_grouping_calls(expr, &head_dropped);
13897                    }
13898                }
13899            }
13900            if let Some(h) = &mut stmt.having {
13901                Self::substitute_grouping_calls(h, &head_dropped);
13902            }
13903            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
13904            // (while `grouping_universe` / the per-branch sets are in scope). For
13905            // each grouping() call in it, inject a per-branch hidden column
13906            // `__grp_ord_K` carrying that branch's mask into the head + every
13907            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
13908            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
13909            // from the final output. A standalone grouping-set query has ORDER BY
13910            // (not an explicit set-op) next, so consuming it here is safe.
13911            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
13912            // rollup carries the hierarchical order: sort by the grouping
13913            // keys with the rolled-up NULLs last, which is exactly the
13914            // interleaving both oracles emit. A client's own ORDER BY wins,
13915            // which is what MySQL does (MariaDB refuses to let one be
13916            // written at all).
13917            // The synthesised keys have to travel the SAME path a written
13918            // ORDER BY does: the block below is what turns a `grouping()`
13919            // call into the per-branch `__grp_ord_K` column the engine can
13920            // actually sort on. Bypassing it left a bare `grouping(text)`
13921            // for the evaluator to reject.
13922            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
13923                self.parse_order_by_keys()?
13924            } else if mysql_rollup {
13925                Self::mysql_rollup_order(&grouping_universe)
13926            } else {
13927                Vec::new()
13928            };
13929            if !synthesised_or_parsed.is_empty() {
13930                let mut order_keys = synthesised_or_parsed;
13931                let mut grp_exprs: Vec<Expr> = Vec::new();
13932                for ob in &order_keys {
13933                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
13934                }
13935                for (k, gexpr) in grp_exprs.iter().enumerate() {
13936                    let colname = alloc::format!("__grp_ord_{k}");
13937                    // Head branch (primary set) uses `head_dropped`.
13938                    let mut he = gexpr.clone();
13939                    Self::substitute_grouping_calls(&mut he, &head_dropped);
13940                    stmt.items.push(SelectItem::Expr {
13941                        expr: he,
13942                        alias: Some(colname.clone()),
13943                    });
13944                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
13945                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
13946                        let set = &grouping_sets[i + 1];
13947                        let dropped: Vec<Expr> = grouping_universe
13948                            .iter()
13949                            .filter(|u| !set.iter().any(|k| k == *u))
13950                            .cloned()
13951                            .collect();
13952                        let mut pe = gexpr.clone();
13953                        Self::substitute_grouping_calls(&mut pe, &dropped);
13954                        peer.items.push(SelectItem::Expr {
13955                            expr: pe,
13956                            alias: Some(colname.clone()),
13957                        });
13958                    }
13959                }
13960                for ob in &mut order_keys {
13961                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
13962                }
13963                stmt.order_by = order_keys;
13964            }
13965        }
13966        Ok(stmt)
13967    }
13968
13969    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
13970    /// as ORDER BY keys.
13971    ///
13972    /// Per key: the rollup marker, then the key. Sorting on the key alone
13973    /// is not enough, and a table with a NULL in it says why — MariaDB puts
13974    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
13975    /// the ROLLUP-introduced NULL last, and both print as NULL.
13976    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
13977    /// real group including the data-NULL one, 1 only for the row the
13978    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
13979    /// rolls up to NULL|2, a|1, b|3, NULL|6.
13980    ///
13981    /// `#[inline(never)]`: its locals must not join the statement parser's
13982    /// frame, which round 430 measured sitting against the nesting guard.
13983    #[inline(never)]
13984    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
13985        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
13986        for e in keys {
13987            out.push(OrderBy {
13988                expr: Expr::FunctionCall {
13989                    name: "grouping".into(),
13990                    args: alloc::vec![e.clone()],
13991                },
13992                desc: false,
13993                nulls_first: None,
13994                collation: None,
13995            });
13996            out.push(OrderBy {
13997                expr: e.clone(),
13998                desc: false,
13999                // MySQL orders NULL first on an ascending key.
14000                nulls_first: Some(true),
14001                collation: None,
14002            });
14003        }
14004        out
14005    }
14006
14007    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14008    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14009    #[inline(never)]
14010    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14011        use crate::ast::MaintainKind;
14012        self.skip_paren_option_list();
14013        let kind = match self.peek() {
14014            // `TABLE` and `INDEX` lex as keywords, not identifiers.
14015            Token::Table | Token::Index => {
14016                self.advance();
14017                MaintainKind::ReindexRelation
14018            }
14019            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14020                "index" | "table" => {
14021                    self.advance();
14022                    MaintainKind::ReindexRelation
14023                }
14024                "schema" => {
14025                    self.advance();
14026                    MaintainKind::ReindexSchema
14027                }
14028                "system" | "database" => {
14029                    self.advance();
14030                    MaintainKind::Whole
14031                }
14032                // PG requires the object type; anything else is the
14033                // caller's problem, not something to swallow.
14034                _ => MaintainKind::ReindexRelation,
14035            },
14036            _ => MaintainKind::Whole,
14037        };
14038        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14039        // allows the plain form, so the modifier is recorded rather than
14040        // skipped. It still has no effect on how the reindex runs.
14041        let mut concurrently = false;
14042        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14043            self.advance();
14044            concurrently = true;
14045        }
14046        let target = self.take_optional_maintain_name();
14047        self.consume_until_statement_boundary();
14048        Ok(Statement::Maintain {
14049            kind,
14050            concurrently,
14051            target,
14052        })
14053    }
14054
14055    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14056    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14057    #[inline(never)]
14058    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14059        use crate::ast::MaintainKind;
14060        self.skip_paren_option_list();
14061        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14062            self.advance();
14063        }
14064        let target = self.take_optional_maintain_name();
14065        self.consume_until_statement_boundary();
14066        Ok(Statement::Maintain {
14067            kind: if target.is_some() {
14068                MaintainKind::ClusterRelation
14069            } else {
14070                MaintainKind::Whole
14071            },
14072            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14073            // transaction block quite happily (measured).
14074            concurrently: false,
14075            target,
14076        })
14077    }
14078
14079    /// The next token as a relation / schema name, when there is one.
14080    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14081        match self.peek() {
14082            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14083                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14084                _ => None,
14085            },
14086            _ => None,
14087        }
14088    }
14089
14090    /// A parenthesised option list, absorbed.
14091    fn skip_paren_option_list(&mut self) {
14092        if !matches!(self.peek(), Token::LParen) {
14093            return;
14094        }
14095        let mut depth = 0usize;
14096        loop {
14097            match self.advance() {
14098                Token::LParen => depth += 1,
14099                Token::RParen => {
14100                    depth -= 1;
14101                    if depth == 0 {
14102                        return;
14103                    }
14104                }
14105                Token::Eof => return,
14106                _ => {}
14107            }
14108        }
14109    }
14110
14111    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14112    /// column list.
14113    ///
14114    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14115    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14116    /// / ALL. The three that describe physical storage have no meaning
14117    /// here, so they parse and change nothing rather than making a
14118    /// dump that mentions them fail to load.
14119    ///
14120    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14121    /// parse chain the nesting sentinel is tuned against.
14122    #[inline(never)]
14123    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14124        self.advance(); // LIKE
14125        let source = self.expect_ident_like()?;
14126        let mut options = crate::ast::LikeOptions::default();
14127        loop {
14128            let including = match self.peek() {
14129                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14130                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14131                _ => break,
14132            };
14133            self.advance();
14134            // `ALL` lexes as its own keyword, not an identifier.
14135            let opt = if matches!(self.peek(), Token::All) {
14136                self.advance();
14137                alloc::string::String::from("all")
14138            } else {
14139                self.expect_ident_like()?
14140            };
14141            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14142                o.defaults = on;
14143                o.constraints = on;
14144                o.identity = on;
14145                o.generated = on;
14146                o.indexes = on;
14147                o.comments = on;
14148            };
14149            match opt.to_ascii_lowercase().as_str() {
14150                "all" => set(&mut options, including),
14151                "defaults" => options.defaults = including,
14152                "constraints" => options.constraints = including,
14153                "identity" => options.identity = including,
14154                "generated" => options.generated = including,
14155                "indexes" => options.indexes = including,
14156                "comments" => options.comments = including,
14157                // No storage model to copy into.
14158                "storage" | "statistics" | "compression" => {}
14159                other => {
14160                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14161                }
14162            }
14163        }
14164        Ok(crate::ast::LikeSpec {
14165            source,
14166            at,
14167            options,
14168        })
14169    }
14170
14171    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14172        // Caller already consumed CREATE; we're sitting on TABLE.
14173        debug_assert!(matches!(self.peek(), Token::Table));
14174        self.advance();
14175        let if_not_exists = self.consume_if_not_exists();
14176        let name = self.expect_ident_like()?;
14177        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14178        // child shape has no column list; the child inherits its
14179        // columns from the parent at engine-DDL time. Detect it
14180        // before the `(` requirement below.
14181        if matches!(self.peek(), Token::Partition)
14182            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14183        {
14184            self.advance(); // PARTITION
14185            self.advance(); // of
14186            let partition_of = self.parse_partition_of_tail()?;
14187            return Ok(Statement::CreateTable(CreateTableStatement {
14188                temporary: false,
14189                name,
14190                columns: Vec::new(),
14191                like_specs: Vec::new(),
14192                inherits: Vec::new(),
14193                if_not_exists,
14194                foreign_keys: Vec::new(),
14195                table_constraints: Vec::new(),
14196                partition_by: None,
14197                partition_of: Some(partition_of),
14198            }));
14199        }
14200        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14201        // the materialized-view materialisation path (run the SELECT, infer the
14202        // column types, create + populate the table) but marks the node so the
14203        // executor creates a plain table without a mat-view registry entry.
14204        if matches!(self.peek(), Token::As) {
14205            self.advance();
14206            let body_stmt = self.parse_select_stmt()?;
14207            let Statement::Select(body) = body_stmt else {
14208                return Err(self.err(format!(
14209                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14210                )));
14211            };
14212            let with_data = self.parse_optional_with_data(true)?;
14213            return Ok(Statement::CreateMaterializedView(
14214                crate::ast::CreateMaterializedViewStatement {
14215                    temporary: false,
14216                    name,
14217                    if_not_exists,
14218                    columns: Vec::new(),
14219                    body,
14220                    with_data,
14221                    as_plain_table: true,
14222                },
14223            ));
14224        }
14225        if !matches!(self.peek(), Token::LParen) {
14226            return Err(self.err(format!(
14227                "expected '(' after table name, got {:?}",
14228                self.peek()
14229            )));
14230        }
14231        self.advance();
14232        let mut columns = Vec::new();
14233        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14234        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14235        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14236        loop {
14237            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14238            // column list. It is how a child that adds nothing of its own is
14239            // written, and this loop demanded at least one entry: `syntax
14240            // error at or near ")"`. The child takes the parent's columns,
14241            // which the INHERITS clause already arranges.
14242            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14243                self.advance();
14244                break;
14245            }
14246            // v7.6.0 / v7.9.18 — distinguish table-level constraint
14247            // clauses from column definitions. Constraints start
14248            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14249            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14250            // a column.
14251            if self.peek_table_level_pk_start() {
14252                table_constraints.push(self.parse_table_level_primary_key()?);
14253            } else if matches!(self.peek(), Token::Like) {
14254                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14255                // <opt> ]*`. The source table's shape lives in the catalog,
14256                // so this records the clause and the engine expands it.
14257                like_specs.push(self.parse_create_table_like(columns.len())?);
14258            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14259                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14260                table_constraints.push(self.parse_table_level_exclude()?);
14261            } else if self.peek_table_level_unique_start() {
14262                table_constraints.push(self.parse_table_level_unique()?);
14263            } else if self.peek_table_level_check_start() {
14264                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14265                table_constraints.push(self.parse_table_level_check()?);
14266            } else if self.peek_mysql_inline_key_start() {
14267                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14268                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14269                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14270                // inside the column list. Skip name + paren list;
14271                // for UNIQUE KEY, register as a UC.
14272                if let Some(uc) = self.parse_mysql_inline_key()? {
14273                    table_constraints.push(uc);
14274                }
14275            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14276                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14277                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14278                // CHECK is named, and the named-CONSTRAINT arm used
14279                // to accept FOREIGN KEY only. The name is accepted
14280                // and discarded — same handling as every other SPG
14281                // constraint name.
14282                self.advance(); // CONSTRAINT
14283                // v7.39 (read01 round 48) — the name is kept now: the schema
14284                // stores it, so DROP / RENAME CONSTRAINT can find it.
14285                let con_name = self.expect_ident_like()?;
14286                let mut tc = match kind {
14287                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14288                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14289                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14290                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14291                };
14292                match &mut tc {
14293                    crate::ast::TableConstraint::Check { name, .. }
14294                    | crate::ast::TableConstraint::Unique { name, .. }
14295                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
14296                    | crate::ast::TableConstraint::Exclude { name, .. } => {
14297                        *name = Some(con_name);
14298                    }
14299                    _ => {}
14300                }
14301                table_constraints.push(tc);
14302            } else if self.peek_constraint_or_fk_start() {
14303                foreign_keys.push(self.parse_table_level_fk()?);
14304            } else {
14305                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14306                // v7.13.0 — fold inline UNIQUE / CHECK column
14307                // constraints into table-level entries so the
14308                // engine path stays uniform.
14309                if col.is_unique {
14310                    table_constraints.push(crate::ast::TableConstraint::Unique {
14311                        name: None,
14312                        columns: alloc::vec![col.name.clone()],
14313                        nulls_not_distinct: col.unique_nulls_not_distinct,
14314                        deferrable: col.constraint_deferrable,
14315                        initially_deferred: col.constraint_initially_deferred,
14316                    });
14317                }
14318                if let Some(check_expr) = col.check.clone() {
14319                    table_constraints.push(crate::ast::TableConstraint::Check {
14320                        name: None,
14321                        expr: check_expr,
14322                        not_valid: false,
14323                    });
14324                }
14325                columns.push(col);
14326                if let Some(fk) = col_level_fk {
14327                    foreign_keys.push(fk);
14328                }
14329            }
14330            match self.peek() {
14331                Token::Comma => {
14332                    self.advance();
14333                }
14334                Token::RParen => {
14335                    self.advance();
14336                    break;
14337                }
14338                other => {
14339                    return Err(
14340                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14341                    );
14342                }
14343            }
14344        }
14345        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14346        // `CREATE TABLE k (LIKE t)` is a complete definition even though
14347        // nothing is written between the parentheses.
14348        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14349        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14350        // empty parentheses were a parse error in their own right — quite apart
14351        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14352        // SPG does not have (filed separately).
14353        let _ = &like_specs;
14354        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14355        // It sits between the column list and the MySQL table options,
14356        // and it was a syntax error until this round.
14357        let mut inherits: Vec<String> = Vec::new();
14358        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14359            if k.eq_ignore_ascii_case("inherits"))
14360        {
14361            self.advance();
14362            if !matches!(self.peek(), Token::LParen) {
14363                return Err(self.err(alloc::format!(
14364                    "expected ( after INHERITS, got {:?}",
14365                    self.peek()
14366                )));
14367            }
14368            self.advance();
14369            loop {
14370                inherits.push(self.expect_ident_like()?);
14371                if matches!(self.peek(), Token::Comma) {
14372                    self.advance();
14373                    continue;
14374                }
14375                break;
14376            }
14377            if !matches!(self.peek(), Token::RParen) {
14378                return Err(self.err(alloc::format!(
14379                    "expected ) closing INHERITS, got {:?}",
14380                    self.peek()
14381                )));
14382            }
14383            self.advance();
14384        }
14385        // v7.14.0 — consume MySQL/MariaDB table options after the
14386        // closing `)`. mysqldump emits things like
14387        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14388        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14389        // SPG accepts all forms as no-ops (each option is
14390        // `<ident> [=] <ident-or-string>` separated by whitespace).
14391        self.consume_mysql_table_options();
14392        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14393        // SPG has no per-table reloptions, so accept and ignore them so a
14394        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14395        self.consume_with_reloptions();
14396        // v7.37.6-B — declarative-partition-parent suffix
14397        // (`PARTITION BY RANGE (key_col)`) sits after the column
14398        // list + MySQL table-options. v7.37.6-B only accepts RANGE
14399        // and locks the key column at one ident; the engine then
14400        // verifies the column type is TIMESTAMPTZ.
14401        let partition_by = if matches!(self.peek(), Token::Partition) {
14402            self.advance(); // PARTITION
14403            if !self.peek_is_by() {
14404                return Err(self.err(format!(
14405                    "expected BY after PARTITION, got {:?}",
14406                    self.peek()
14407                )));
14408            }
14409            self.advance();
14410            Some(self.parse_partition_by_tail()?)
14411        } else {
14412            None
14413        };
14414        Ok(Statement::CreateTable(CreateTableStatement {
14415            temporary: false,
14416            name,
14417            columns,
14418            like_specs,
14419            inherits,
14420            if_not_exists,
14421            foreign_keys,
14422            table_constraints,
14423            partition_by,
14424            partition_of: None,
14425        }))
14426    }
14427
14428    /// v7.37.6-B — case-insensitive ident match helper for the
14429    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14430    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14431    /// didn't burn a global keyword slot for each (see the
14432    /// `Token::Partition` doc-comment in `lexer.rs`).
14433    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14434        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14435    }
14436
14437    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14438    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14439    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14440        use crate::ast::{PartitionBySpec, PartitionKindAst};
14441        let kind = match self.peek() {
14442            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14443                self.advance();
14444                PartitionKindAst::Range
14445            }
14446            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14447                self.advance();
14448                PartitionKindAst::List
14449            }
14450            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14451                self.advance();
14452                PartitionKindAst::Hash
14453            }
14454            other => {
14455                return Err(self.err(format!(
14456                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14457                )));
14458            }
14459        };
14460        if !matches!(self.peek(), Token::LParen) {
14461            return Err(self.err(format!(
14462                "expected '(' after PARTITION BY <strategy>, got {:?}",
14463                self.peek()
14464            )));
14465        }
14466        self.advance();
14467        let mut key_columns = Vec::new();
14468        loop {
14469            key_columns.push(self.expect_ident_like()?);
14470            match self.peek() {
14471                Token::Comma => {
14472                    self.advance();
14473                }
14474                Token::RParen => {
14475                    self.advance();
14476                    break;
14477                }
14478                other => {
14479                    return Err(self.err(format!(
14480                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14481                    )));
14482                }
14483            }
14484        }
14485        if key_columns.is_empty() {
14486            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14487        }
14488        Ok(PartitionBySpec { kind, key_columns })
14489    }
14490
14491    /// v7.37.6-B — after `PARTITION OF`, expect
14492    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14493    /// or
14494    ///   <parent> DEFAULT
14495    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14496        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14497        let parent_name = self.expect_ident_like()?;
14498        // v7.37.6-B rejects an explicit column list — the child
14499        // inherits from the parent. mailrs round-7 taught us that
14500        // CREATE TABLE-side schema reconciliation hides drift, so
14501        // we surface this as a parse error rather than silently
14502        // ignoring user columns.
14503        if matches!(self.peek(), Token::LParen) {
14504            return Err(self.err(
14505                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14506                 at v7.37.6-B; the child inherits its columns from the parent"
14507                    .to_string(),
14508            ));
14509        }
14510        let bounds = match self.peek() {
14511            Token::Default => {
14512                self.advance();
14513                PartitionOfBoundsAst::Default
14514            }
14515            Token::For => {
14516                self.advance();
14517                if !matches!(self.peek(), Token::Values) {
14518                    return Err(
14519                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14520                    );
14521                }
14522                self.advance();
14523                // WITH is not a reserved Token in the lexer — it lexes
14524                // as Token::Ident("with"). Disambiguate manually.
14525                let want_with = matches!(
14526                    self.peek(),
14527                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14528                );
14529                if want_with {
14530                    self.advance();
14531                    if !matches!(self.peek(), Token::LParen) {
14532                        return Err(self.err(format!(
14533                            "expected '(' after FOR VALUES WITH, got {:?}",
14534                            self.peek()
14535                        )));
14536                    }
14537                    self.advance();
14538                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14539                    loop {
14540                        let key = self.expect_ident_like()?;
14541                        let n = match self.peek().clone() {
14542                            Token::Integer(v) if u32::try_from(v).is_ok() => {
14543                                self.advance();
14544                                v as u32
14545                            }
14546                            other => {
14547                                return Err(self.err(format!(
14548                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14549                                )));
14550                            }
14551                        };
14552                        match key.to_ascii_uppercase().as_str() {
14553                            "MODULUS" => modulus = Some(n),
14554                            "REMAINDER" => remainder = Some(n),
14555                            other => {
14556                                return Err(self.err(format!(
14557                                    "FOR VALUES WITH: unknown key {other:?}; \
14558                                     expected MODULUS or REMAINDER"
14559                                )));
14560                            }
14561                        }
14562                        match self.peek() {
14563                            Token::Comma => {
14564                                self.advance();
14565                            }
14566                            Token::RParen => {
14567                                self.advance();
14568                                break;
14569                            }
14570                            other => {
14571                                return Err(self.err(format!(
14572                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14573                                )));
14574                            }
14575                        }
14576                    }
14577                    let modulus = modulus
14578                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14579                    let remainder = remainder.ok_or_else(|| {
14580                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14581                    })?;
14582                    if modulus == 0 {
14583                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14584                    }
14585                    if remainder >= modulus {
14586                        return Err(self.err(format!(
14587                            "FOR VALUES WITH: REMAINDER ({remainder}) \
14588                             must be < MODULUS ({modulus})"
14589                        )));
14590                    }
14591                    PartitionOfBoundsAst::Hash { modulus, remainder }
14592                } else {
14593                    match self.peek() {
14594                        Token::From => {
14595                            self.advance();
14596                            let lower = Box::new(self.parse_partition_bound_expr()?);
14597                            if !matches!(self.peek(), Token::To) {
14598                                return Err(self.err(format!(
14599                                    "expected TO after FROM (...), got {:?}",
14600                                    self.peek()
14601                                )));
14602                            }
14603                            self.advance();
14604                            let upper = Box::new(self.parse_partition_bound_expr()?);
14605                            PartitionOfBoundsAst::Range { lower, upper }
14606                        }
14607                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14608                        Token::In => {
14609                            self.advance();
14610                            if !matches!(self.peek(), Token::LParen) {
14611                                return Err(self.err(format!(
14612                                    "expected '(' after FOR VALUES IN, got {:?}",
14613                                    self.peek()
14614                                )));
14615                            }
14616                            self.advance();
14617                            let mut values = Vec::new();
14618                            loop {
14619                                values.push(self.parse_expr(0)?);
14620                                match self.peek() {
14621                                    Token::Comma => {
14622                                        self.advance();
14623                                    }
14624                                    Token::RParen => {
14625                                        self.advance();
14626                                        break;
14627                                    }
14628                                    other => {
14629                                        return Err(self.err(format!(
14630                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14631                                    )));
14632                                    }
14633                                }
14634                            }
14635                            if values.is_empty() {
14636                                return Err(self.err(
14637                                    "FOR VALUES IN requires at least one literal".to_string(),
14638                                ));
14639                            }
14640                            PartitionOfBoundsAst::List { values }
14641                        }
14642                        other => {
14643                            return Err(self.err(format!(
14644                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14645                            )));
14646                        }
14647                    }
14648                }
14649            }
14650            other => {
14651                return Err(self.err(format!(
14652                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14653                )));
14654            }
14655        };
14656        Ok(PartitionOfSpec {
14657            parent_name,
14658            bounds,
14659        })
14660    }
14661
14662    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14663    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14664    /// markers (no-arg builtins) so the engine resolves them
14665    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14666    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14667        if !matches!(self.peek(), Token::LParen) {
14668            return Err(self.err(format!(
14669                "expected '(' before partition bound, got {:?}",
14670                self.peek()
14671            )));
14672        }
14673        self.advance();
14674        let expr = match self.peek() {
14675            Token::Ident(s) | Token::QuotedIdent(s)
14676                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14677            {
14678                let name = s.to_ascii_uppercase();
14679                self.advance();
14680                crate::ast::Expr::FunctionCall {
14681                    name,
14682                    args: Vec::new(),
14683                }
14684            }
14685            _ => self.parse_expr(0)?,
14686        };
14687        if !matches!(self.peek(), Token::RParen) {
14688            return Err(self.err(format!(
14689                "expected ')' after partition bound, got {:?}",
14690                self.peek()
14691            )));
14692        }
14693        self.advance();
14694        Ok(expr)
14695    }
14696
14697    /// v7.14.0 — true when the next tokens look like an inline
14698    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14699    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14700    /// — each followed by an optional name + `(...)`. Critical:
14701    /// a column NAMED `key` / `index` (PG accepts as ident) must
14702    /// NOT be mistaken for the KEY constraint shape. We disambig
14703    /// by requiring the keyword to be followed by either `(` or
14704    /// `<ident> (`.
14705    fn peek_mysql_inline_key_start(&self) -> bool {
14706        let cur = self.peek();
14707        // Shapes:
14708        //   KEY (cols)
14709        //   KEY name (cols)
14710        //   INDEX (cols)
14711        //   INDEX name (cols)
14712        //   UNIQUE KEY [name] (cols)
14713        //   UNIQUE INDEX [name] (cols)
14714        //   FULLTEXT [KEY|INDEX] [name] (cols)
14715        //   SPATIAL [KEY|INDEX] [name] (cols)
14716        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14717            // tokens at skip = the position AFTER the index-form
14718            // keywords (KEY/INDEX) have been consumed.
14719            match self.tokens.get(skip) {
14720                Some(Token::LParen) => true,
14721                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14722                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14723                }
14724                _ => false,
14725            }
14726        };
14727        // `INDEX` lexes as Token::Index (reserved), not as
14728        // Token::Ident("index"). Both shapes count as a KEY/INDEX
14729        // start; the peek helper below handles either.
14730        let is_key_or_index_tok = |t: &Token| -> bool {
14731            matches!(t, Token::Index)
14732                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14733        };
14734        match cur {
14735            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14736            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14737                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14738            }
14739            Token::Ident(s)
14740                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14741            {
14742                let nxt = self.tokens.get(self.pos + 1);
14743                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14744                    self.pos + 2
14745                } else {
14746                    self.pos + 1
14747                };
14748                after_keyword_followed_by_paren_or_ident_paren(after_after)
14749            }
14750            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14751                let nxt = self.tokens.get(self.pos + 1);
14752                if !nxt.is_some_and(is_key_or_index_tok) {
14753                    return false;
14754                }
14755                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14756            }
14757            _ => false,
14758        }
14759    }
14760
14761    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14762    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14763    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14764    /// returns Some(TableConstraint::Index) so the engine builds
14765    /// a real BTree index on the leading column (mysqldump
14766    /// `KEY idx_posts_author (author_id)` shape).
14767    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14768    /// (the storage layer has no matching AM).
14769    fn parse_mysql_inline_key(
14770        &mut self,
14771    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14772        // Detect UNIQUE prefix.
14773        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14774        {
14775            self.advance();
14776            true
14777        } else {
14778            false
14779        };
14780        // Consume FULLTEXT / SPATIAL prefix and record which one
14781        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14782        // dedicated TableConstraint variant so the engine can
14783        // build a tsvector-GIN; SPATIAL still has no matching
14784        // AM, so it falls back to accept-as-no-op.
14785        let mut is_fulltext = false;
14786        let mut is_spatial = false;
14787        if let Token::Ident(s) = self.peek().clone() {
14788            if s.eq_ignore_ascii_case("fulltext") {
14789                self.advance();
14790                is_fulltext = true;
14791            } else if s.eq_ignore_ascii_case("spatial") {
14792                self.advance();
14793                is_spatial = true;
14794            }
14795        }
14796        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14797        // (reserved); accept either token shape.
14798        match self.peek() {
14799            Token::Index => {
14800                self.advance();
14801            }
14802            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14803                self.advance();
14804            }
14805            other => {
14806                return Err(self.err(alloc::format!(
14807                    "expected KEY/INDEX in inline index declaration, got {other:?}"
14808                )));
14809            }
14810        }
14811        // Optional index name (an ident before the `(`).
14812        // v7.15.0 — capture the name when present so the engine
14813        // builds the secondary index under the user's chosen
14814        // name (matches mysqldump's `KEY idx_x (col)` shape).
14815        let mut idx_name: Option<String> = None;
14816        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
14817            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
14818        {
14819            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
14820                idx_name = Some(s);
14821            }
14822        }
14823        // Optional `USING BTREE` / `USING HASH` (MySQL).
14824        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14825            self.advance();
14826            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14827                self.advance();
14828            }
14829        }
14830        // Required column list `(col [, col]*)`.
14831        if !matches!(self.peek(), Token::LParen) {
14832            return Err(self.err(alloc::format!(
14833                "expected '(' in inline KEY/INDEX, got {:?}",
14834                self.peek()
14835            )));
14836        }
14837        self.advance();
14838        let mut cols: Vec<String> = Vec::new();
14839        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
14840            self.advance();
14841            cols.push(s);
14842            // Skip optional `(length)` per-column prefix.
14843            if matches!(self.peek(), Token::LParen) {
14844                let mut depth = 1usize;
14845                self.advance();
14846                while depth > 0 {
14847                    match self.peek() {
14848                        Token::LParen => depth += 1,
14849                        Token::RParen => depth -= 1,
14850                        Token::Eof => break,
14851                        _ => {}
14852                    }
14853                    self.advance();
14854                }
14855            }
14856            // Skip optional ASC / DESC.
14857            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
14858                || matches!(self.peek(), Token::Asc | Token::Desc)
14859            {
14860                self.advance();
14861            }
14862            if matches!(self.peek(), Token::Comma) {
14863                self.advance();
14864                continue;
14865            }
14866            break;
14867        }
14868        if matches!(self.peek(), Token::RParen) {
14869            self.advance();
14870        }
14871        // Trailing options on the inline index — comment / etc.
14872        // Skip until comma or `)`.
14873        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
14874            self.advance();
14875        }
14876        if cols.is_empty() {
14877            return Ok(None);
14878        }
14879        if is_unique {
14880            // Carry the captured idx_name on UNIQUE too so future
14881            // engine work can name the underlying BTree
14882            // accordingly; today the unique-constraint installer
14883            // synthesises the name itself, but Display round-trip
14884            // benefits from preserving it.
14885            Ok(Some(crate::ast::TableConstraint::Unique {
14886                name: idx_name,
14887                columns: cols,
14888                nulls_not_distinct: false,
14889                // MySQL inline UNIQUE KEY has no deferral vocabulary.
14890                deferrable: false,
14891                initially_deferred: false,
14892            }))
14893        } else if is_fulltext {
14894            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
14895            // routes through `TableConstraint::FulltextIndex`;
14896            // the engine builds a tsvector-GIN over each named
14897            // column so MATCH AGAINST gets a real inverted
14898            // index instead of a silently-dropped declaration.
14899            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
14900                name: idx_name,
14901                columns: cols,
14902            }))
14903        } else if is_spatial {
14904            // SPG has no native SPATIAL AM. Accept-as-no-op
14905            // (declaration is parsed, but no index is built).
14906            Ok(None)
14907        } else {
14908            // v7.15.0 — plain KEY / INDEX builds a real BTree
14909            // secondary index.
14910            Ok(Some(crate::ast::TableConstraint::Index {
14911                name: idx_name,
14912                columns: cols,
14913            }))
14914        }
14915    }
14916
14917    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
14918    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
14919    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
14920    /// (in any order, separated by whitespace).
14921    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
14922    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
14923    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
14924    /// bare ident here, and only the parenthesised form is reloptions (so this
14925    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
14926    fn consume_with_reloptions(&mut self) {
14927        let is_with = matches!(
14928            self.peek(),
14929            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14930        );
14931        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
14932            return;
14933        }
14934        self.advance(); // WITH
14935        self.advance(); // (
14936        let mut depth = 1u32;
14937        while depth > 0 && !matches!(self.peek(), Token::Eof) {
14938            match self.peek() {
14939                Token::LParen => depth += 1,
14940                Token::RParen => depth -= 1,
14941                _ => {}
14942            }
14943            self.advance();
14944        }
14945    }
14946
14947    fn consume_mysql_table_options(&mut self) {
14948        loop {
14949            // Heuristic: a table option is an ident (or `DEFAULT`
14950            // reserved keyword) followed by `=` and an
14951            // ident / string / integer.
14952            let name_lc = match self.peek().clone() {
14953                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
14954                Token::Default => alloc::string::String::from("default"),
14955                _ => break,
14956            };
14957            let known = matches!(
14958                name_lc.as_str(),
14959                "engine"
14960                    | "default"
14961                    | "charset"
14962                    | "collate"
14963                    | "auto_increment"
14964                    | "row_format"
14965                    | "comment"
14966                    | "pack_keys"
14967                    | "stats_persistent"
14968                    | "stats_auto_recalc"
14969                    | "stats_sample_pages"
14970                    | "key_block_size"
14971                    | "tablespace"
14972                    | "min_rows"
14973                    | "max_rows"
14974                    | "checksum"
14975                    | "delay_key_write"
14976                    | "insert_method"
14977                    | "data"
14978                    | "index"
14979                    | "encryption"
14980                    | "compression"
14981            );
14982            if !known {
14983                break;
14984            }
14985            self.advance(); // option name
14986            // `DEFAULT` optional prefix is followed by `CHARSET` /
14987            // `COLLATE`; consume the next ident too.
14988            if name_lc == "default" {
14989                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14990                    self.advance();
14991                }
14992            }
14993            if matches!(self.peek(), Token::Eq) {
14994                self.advance();
14995            }
14996            match self.peek() {
14997                Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
14998                    self.advance();
14999                }
15000                _ => {}
15001            }
15002        }
15003    }
15004
15005    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15006    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15007    /// sure (otherwise a column literally named `primary` would
15008    /// be mistaken).
15009    fn peek_table_level_pk_start(&self) -> bool {
15010        let cur = self.peek();
15011        let nxt = self.tokens.get(self.pos + 1);
15012        let nxt2 = self.tokens.get(self.pos + 2);
15013        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15014        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15015        let is_lparen = matches!(nxt2, Some(Token::LParen));
15016        is_primary && is_key && is_lparen
15017    }
15018
15019    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15020    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15021    /// (mailrs round-5 G10).
15022    fn peek_table_level_unique_start(&self) -> bool {
15023        let cur = self.peek();
15024        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15025        if !is_unique {
15026            return false;
15027        }
15028        let n1 = self.tokens.get(self.pos + 1);
15029        // Plain `UNIQUE (…)`.
15030        if matches!(n1, Some(Token::LParen)) {
15031            return true;
15032        }
15033        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15034        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15035        if !is_nulls {
15036            return false;
15037        }
15038        let n2 = self.tokens.get(self.pos + 2);
15039        let n3 = self.tokens.get(self.pos + 3);
15040        let n4 = self.tokens.get(self.pos + 4);
15041        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15042        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15043            return true;
15044        }
15045        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15046        if matches!(n2, Some(Token::Not))
15047            && matches!(n3, Some(Token::Distinct))
15048            && matches!(n4, Some(Token::LParen))
15049        {
15050            return true;
15051        }
15052        false
15053    }
15054
15055    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15056        self.advance(); // PRIMARY
15057        self.advance(); // KEY
15058        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15059        // v7.39 (round 711) — the trailer's values are CARRIED now; round
15060        // 621 consumed and dropped them (the storing half of F08).
15061        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15062        Ok(crate::ast::TableConstraint::PrimaryKey {
15063            name: None,
15064            columns,
15065            deferrable,
15066            initially_deferred,
15067        })
15068    }
15069
15070    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15071        self.advance(); // UNIQUE
15072        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15073        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15074        // is `NULLS DISTINCT` per the SQL standard.
15075        let mut nulls_not_distinct = false;
15076        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15077            let n1 = self.tokens.get(self.pos + 1);
15078            let n2 = self.tokens.get(self.pos + 2);
15079            let is_not = matches!(n1, Some(Token::Not));
15080            let is_distinct = matches!(n2, Some(Token::Distinct));
15081            if is_not && is_distinct {
15082                self.advance(); // NULLS
15083                self.advance(); // NOT
15084                self.advance(); // DISTINCT
15085                nulls_not_distinct = true;
15086            } else if matches!(n1, Some(Token::Distinct)) {
15087                self.advance(); // NULLS
15088                self.advance(); // DISTINCT
15089            }
15090        }
15091        let columns = self.parse_paren_ident_list("UNIQUE")?;
15092        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15093        Ok(crate::ast::TableConstraint::Unique {
15094            name: None,
15095            columns,
15096            nulls_not_distinct,
15097            deferrable,
15098            initially_deferred,
15099        })
15100    }
15101
15102    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15103    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15104    /// expression.
15105    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15106    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15107    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15108    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15109    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15110    /// commit: `NOT` starts no other suffix here, but reading both
15111    /// tokens before advancing keeps the caller's error message intact
15112    /// if someone writes `NOT NULL` by mistake.
15113    fn parse_not_valid_suffix(&mut self) -> bool {
15114        if !matches!(self.peek(), Token::Not) {
15115            return false;
15116        }
15117        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15118        {
15119            return false;
15120        }
15121        self.advance();
15122        self.advance();
15123        true
15124    }
15125
15126    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15127        self.advance(); // EXCLUDE
15128        // Optional `USING <method>`.
15129        let mut method = None;
15130        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15131            self.advance();
15132            method = Some(match self.advance() {
15133                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15134                other => {
15135                    return Err(self.err(alloc::format!(
15136                        "expected index method after USING, got {other:?}"
15137                    )));
15138                }
15139            });
15140        }
15141        if !matches!(self.peek(), Token::LParen) {
15142            return Err(self.err(alloc::format!(
15143                "expected '(' after EXCLUDE, got {:?}",
15144                self.peek()
15145            )));
15146        }
15147        self.advance();
15148        let mut elements: Vec<(String, String)> = Vec::new();
15149        loop {
15150            let col = match self.advance() {
15151                Token::Ident(s) | Token::QuotedIdent(s) => s,
15152                other => {
15153                    return Err(self.err(alloc::format!(
15154                        "expected column name in EXCLUDE, got {other:?}"
15155                    )));
15156                }
15157            };
15158            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15159                return Err(self.err(alloc::format!(
15160                    "expected WITH after EXCLUDE column, got {:?}",
15161                    self.peek()
15162                )));
15163            }
15164            self.advance();
15165            let op = match self.advance() {
15166                Token::InetOverlap => String::from("&&"),
15167                Token::Intersects => String::from("?#"),
15168                Token::IsBelow => String::from("<^"),
15169                Token::IsAbove => String::from(">^"),
15170                Token::PatternLt => String::from("~<~"),
15171                Token::PatternLtEq => String::from("~<=~"),
15172                Token::PatternGt => String::from("~>~"),
15173                Token::PatternGtEq => String::from("~>=~"),
15174                Token::TsMatchOld => String::from("@@@"),
15175                Token::Eq => String::from("="),
15176                Token::JsonContains => String::from("@>"),
15177                Token::JsonContainedBy => String::from("<@"),
15178                Token::OverLeft => String::from("&<"),
15179                Token::OverRight => String::from("&>"),
15180                other => {
15181                    return Err(self.err(alloc::format!(
15182                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15183                    )));
15184                }
15185            };
15186            elements.push((col, op));
15187            if matches!(self.peek(), Token::Comma) {
15188                self.advance();
15189                continue;
15190            }
15191            break;
15192        }
15193        if !matches!(self.peek(), Token::RParen) {
15194            return Err(self.err(alloc::format!(
15195                "expected ')' to close EXCLUDE, got {:?}",
15196                self.peek()
15197            )));
15198        }
15199        self.advance();
15200        Ok(crate::ast::TableConstraint::Exclude {
15201            name: None,
15202            method,
15203            elements,
15204        })
15205    }
15206
15207    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15208        self.advance(); // CHECK
15209        if !matches!(self.peek(), Token::LParen) {
15210            return Err(self.err(alloc::format!(
15211                "expected '(' after CHECK, got {:?}",
15212                self.peek()
15213            )));
15214        }
15215        self.advance();
15216        let expr = self.parse_expr(0)?;
15217        if !matches!(self.peek(), Token::RParen) {
15218            return Err(self.err(alloc::format!(
15219                "expected ')' to close CHECK predicate, got {:?}",
15220                self.peek()
15221            )));
15222        }
15223        self.advance();
15224        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15225        // are no existing rows for PG to skip, so it rejects the suffix.
15226        Ok(crate::ast::TableConstraint::Check {
15227            name: None,
15228            expr,
15229            not_valid: false,
15230        })
15231    }
15232
15233    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15234    fn peek_table_level_check_start(&self) -> bool {
15235        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15236    }
15237
15238    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15239    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15240    /// on the dedicated FK path (`parse_table_level_fk` consumes its
15241    /// own CONSTRAINT prefix).
15242    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15243        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15244            return None;
15245        }
15246        // tokens[pos+1] is the constraint name (any ident-like);
15247        // tokens[pos+2] is the kind keyword.
15248        match self.tokens.get(self.pos + 2) {
15249            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15250                Some(NamedTableConstraintKind::Check)
15251            }
15252            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15253                Some(NamedTableConstraintKind::Unique)
15254            }
15255            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15256                Some(NamedTableConstraintKind::PrimaryKey)
15257            }
15258            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15259                Some(NamedTableConstraintKind::Exclude)
15260            }
15261            _ => None,
15262        }
15263    }
15264
15265    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15266        if !matches!(self.peek(), Token::LParen) {
15267            return Err(self.err(alloc::format!(
15268                "expected '(' after {ctx}, got {:?}",
15269                self.peek()
15270            )));
15271        }
15272        self.advance();
15273        let mut out = Vec::new();
15274        loop {
15275            out.push(self.expect_ident_like()?);
15276            match self.peek() {
15277                Token::Comma => {
15278                    self.advance();
15279                }
15280                Token::RParen => {
15281                    self.advance();
15282                    break;
15283                }
15284                other => {
15285                    return Err(self.err(alloc::format!(
15286                        "expected ',' or ')' in {ctx} list, got {other:?}"
15287                    )));
15288                }
15289            }
15290        }
15291        if out.is_empty() {
15292            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15293        }
15294        Ok(out)
15295    }
15296
15297    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15298    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15299    /// table-level FK; a column def never starts with either keyword
15300    /// (column names are not in this reserved set).
15301    fn peek_constraint_or_fk_start(&self) -> bool {
15302        let is_constraint_kw = matches!(
15303            self.peek(),
15304            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15305        );
15306        let is_foreign_kw = matches!(
15307            self.peek(),
15308            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15309        );
15310        is_constraint_kw || is_foreign_kw
15311    }
15312
15313    /// v7.6.0 — parse a table-level FK clause:
15314    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15315    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15316    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15317        let mut name: Option<String> = None;
15318        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15319            self.advance();
15320            name = Some(self.expect_ident_like()?);
15321        }
15322        // `FOREIGN`
15323        match self.advance() {
15324            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15325            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15326        }
15327        // `KEY`
15328        match self.advance() {
15329            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15330            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15331        }
15332        // `(col, col, ...)`
15333        if !matches!(self.peek(), Token::LParen) {
15334            return Err(self.err(format!(
15335                "expected '(' after FOREIGN KEY, got {:?}",
15336                self.peek()
15337            )));
15338        }
15339        self.advance();
15340        let mut columns = Vec::new();
15341        loop {
15342            columns.push(self.expect_ident_like()?);
15343            match self.peek() {
15344                Token::Comma => {
15345                    self.advance();
15346                }
15347                Token::RParen => {
15348                    self.advance();
15349                    break;
15350                }
15351                other => {
15352                    return Err(self.err(format!(
15353                        "expected ',' or ')' in FK column list, got {other:?}"
15354                    )));
15355                }
15356            }
15357        }
15358        if columns.is_empty() {
15359            return Err(self.err("FOREIGN KEY requires at least one column".into()));
15360        }
15361        let (
15362            parent_table,
15363            parent_columns,
15364            on_delete,
15365            on_update,
15366            match_type,
15367            deferrable,
15368            initially_deferred,
15369        ) = self.parse_references_tail(columns.len())?;
15370        Ok(ForeignKeyConstraint {
15371            name,
15372            columns,
15373            parent_table,
15374            parent_columns,
15375            on_delete,
15376            on_update,
15377            match_type,
15378            deferrable,
15379            initially_deferred,
15380        })
15381    }
15382
15383    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15384    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15385    /// the local column count, used to default the parent column
15386    /// list when omitted (SQL spec: parent's PK is implied).
15387    fn parse_references_tail(
15388        &mut self,
15389        expected_arity: usize,
15390    ) -> Result<
15391        (
15392            String,
15393            Vec<String>,
15394            FkAction,
15395            FkAction,
15396            crate::ast::MatchType,
15397            // v7.39 (round 288) — deferrable, initially_deferred.
15398            bool,
15399            bool,
15400        ),
15401        ParseError,
15402    > {
15403        match self.advance() {
15404            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15405            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15406        }
15407        let parent_table = self.expect_ident_like()?;
15408        let mut parent_columns: Vec<String> = Vec::new();
15409        if matches!(self.peek(), Token::LParen) {
15410            self.advance();
15411            loop {
15412                parent_columns.push(self.expect_ident_like()?);
15413                match self.peek() {
15414                    Token::Comma => {
15415                        self.advance();
15416                    }
15417                    Token::RParen => {
15418                        self.advance();
15419                        break;
15420                    }
15421                    other => {
15422                        return Err(self.err(format!(
15423                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
15424                        )));
15425                    }
15426                }
15427            }
15428        }
15429        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15430            return Err(self.err(format!(
15431                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15432                expected_arity,
15433                parent_columns.len()
15434            )));
15435        }
15436        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15437        // it between the referenced column list and the ON / DEFERRABLE
15438        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15439        // is skipped when any referencing column is NULL), so SIMPLE —
15440        // the default, and the only spelling pg_dump emits — is accepted
15441        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15442        // mixed-NULL rule, which is not wired yet; reject them honestly
15443        // rather than silently applying SIMPLE (PG itself errors on
15444        // MATCH PARTIAL as "not yet implemented").
15445        let mut match_type = crate::ast::MatchType::Simple;
15446        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15447            self.advance();
15448            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15449            // SIMPLE / PARTIAL arrive as bare identifiers.
15450            let kind = match self.advance() {
15451                Token::Full => "FULL".to_string(),
15452                Token::Ident(s) => s.to_uppercase(),
15453                other => {
15454                    return Err(self.err(format!(
15455                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15456                    )));
15457                }
15458            };
15459            match kind.as_str() {
15460                "SIMPLE" => {} // Default — match_type stays Simple.
15461                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15462                // when ALL referencing columns are NULL; a mixed-NULL key errors.
15463                "FULL" => match_type = crate::ast::MatchType::Full,
15464                "PARTIAL" => {
15465                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15466                }
15467                _ => {
15468                    return Err(self.err(format!(
15469                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15470                    )));
15471                }
15472            }
15473        }
15474        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15475        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15476        // <action>` / `ON UPDATE <action>` in either order. PG /
15477        // pg_dump emits the timing clause AFTER the ON clauses
15478        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15479        // but the SQL spec allows either order. We loop over
15480        // every possible trailer and dispatch on the next token,
15481        // stopping when nothing matches. Phase 3.1 changes the
15482        // bare DEFERRABLE form from hard-error to accept-as-
15483        // immediate; SPG is single-writer with no deferred-
15484        // constraint window so the runtime semantics are always
15485        // immediate even when INITIALLY DEFERRED is requested.
15486        // PG's default referential action (no ON DELETE / ON UPDATE
15487        // clause) is NO ACTION, not RESTRICT — the two enforce
15488        // identically in SPG (single-writer, no deferred window; see the
15489        // shared match arm in constraints.rs) but information_schema.
15490        // referential_constraints must report NO ACTION to match PG.
15491        let mut on_delete = FkAction::NoAction;
15492        let mut on_update = FkAction::NoAction;
15493        let mut seen_on_delete = false;
15494        let mut seen_on_update = false;
15495        let mut deferrable = false;
15496        let mut initially_deferred = false;
15497        loop {
15498            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15499            let before = self.pos;
15500            let (d, idef) = self.consume_deferrable_clauses_timed()?;
15501            if self.pos != before {
15502                deferrable = d;
15503                initially_deferred = idef;
15504                continue;
15505            }
15506            // ON DELETE / ON UPDATE.
15507            if !matches!(self.peek(), Token::On) {
15508                break;
15509            }
15510            self.advance();
15511            let which = self.advance();
15512            let action = self.parse_fk_action()?;
15513            match which {
15514                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15515                    if seen_on_delete {
15516                        return Err(self.err("ON DELETE specified twice".into()));
15517                    }
15518                    seen_on_delete = true;
15519                    on_delete = action;
15520                }
15521                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15522                    if seen_on_update {
15523                        return Err(self.err("ON UPDATE specified twice".into()));
15524                    }
15525                    seen_on_update = true;
15526                    on_update = action;
15527                }
15528                other => {
15529                    return Err(
15530                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15531                    );
15532                }
15533            }
15534        }
15535        Ok((
15536            parent_table,
15537            parent_columns,
15538            on_delete,
15539            on_update,
15540            match_type,
15541            deferrable,
15542            initially_deferred,
15543        ))
15544    }
15545
15546    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15547    /// NO ACTION`.
15548    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15549        match self.advance() {
15550            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15551            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15552            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15553                Token::Null => Ok(FkAction::SetNull),
15554                Token::Default => Ok(FkAction::SetDefault),
15555                other => Err(self.err(format!(
15556                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15557                ))),
15558            },
15559            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15560                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15561                other => Err(self.err(format!(
15562                    "expected ACTION after NO in FK action, got {other:?}"
15563                ))),
15564            },
15565            other => Err(self.err(format!(
15566                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15567            ))),
15568        }
15569    }
15570
15571    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15572    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15573    fn consume_if_not_exists(&mut self) -> bool {
15574        // `IF` arrives as a bare Ident (we don't reserve it because it
15575        // also appears mid-expression in PG, though we don't support
15576        // those forms yet).
15577        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15578        if !looks_like_if {
15579            return false;
15580        }
15581        // Peek one ahead before committing: only consume IF when it's
15582        // actually `IF NOT EXISTS`.
15583        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15584            return false;
15585        }
15586        if !matches!(
15587            self.tokens.get(self.pos + 2),
15588            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15589        ) {
15590            return false;
15591        }
15592        self.advance(); // IF
15593        self.advance(); // NOT
15594        self.advance(); // EXISTS
15595        true
15596    }
15597
15598    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15599    /// Consumes IF EXISTS as a pair; returns false otherwise
15600    /// without consuming any tokens.
15601    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15602    /// ENABLE/DISABLE/FORCE/NO FORCE.
15603    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15604        for kw in ["row", "level", "security"] {
15605            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15606            {
15607                return Err(self.err(alloc::format!(
15608                    "expected {} in ROW LEVEL SECURITY, got {:?}",
15609                    kw.to_ascii_uppercase(),
15610                    self.peek()
15611                )));
15612            }
15613            self.advance();
15614        }
15615        Ok(())
15616    }
15617
15618    fn consume_if_exists(&mut self) -> bool {
15619        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15620        if !looks_like_if {
15621            return false;
15622        }
15623        if !matches!(
15624            self.tokens.get(self.pos + 1),
15625            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15626        ) {
15627            return false;
15628        }
15629        self.advance(); // IF
15630        self.advance(); // EXISTS
15631        true
15632    }
15633
15634    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15635    /// qualifiers after an index column ref. ASC / DESC are
15636    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15637    /// We accept and discard them since single-column BTree
15638    /// stores rows in natural key order today.
15639    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15640    /// ORDER BY key. Returns None when absent.
15641    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15642        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15643            return Ok(None);
15644        }
15645        self.advance();
15646        match self.advance() {
15647            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15648            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15649            other => Err(self.err(alloc::format!(
15650                "expected FIRST or LAST after NULLS, got {other:?}"
15651            ))),
15652        }
15653    }
15654
15655    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15656    /// rather than discarded.
15657    ///
15658    /// SPG's index does not scan in a direction — column ordering is
15659    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15660    /// reproduction of the DDL, and dropping the clause meant
15661    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15662    /// dump lost it, and a schema diff saw drift on every run.
15663    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15664        let mut order = crate::ast::IndexColumnOrder::default();
15665        loop {
15666            match self.peek() {
15667                Token::Asc => {
15668                    self.advance();
15669                }
15670                Token::Desc => {
15671                    order.descending = true;
15672                    self.advance();
15673                }
15674                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15675                    let look = self.tokens.get(self.pos + 1);
15676                    if matches!(
15677                        look,
15678                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15679                            || k.eq_ignore_ascii_case("last")
15680                    ) {
15681                        self.advance();
15682                        order.nulls_first = Some(matches!(
15683                            self.advance(),
15684                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
15685                        ));
15686                    } else {
15687                        break;
15688                    }
15689                }
15690                _ => break,
15691            }
15692        }
15693        order
15694    }
15695
15696    fn parse_create_index_stmt_after_create(
15697        &mut self,
15698        is_unique: bool,
15699    ) -> Result<Statement, ParseError> {
15700        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15701        debug_assert!(matches!(self.peek(), Token::Index));
15702        self.advance();
15703        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15704        // SPG's CREATE INDEX is synchronous end-to-end today (real
15705        // CONCURRENTLY variant with restartable scans queues with
15706        // v7.39 indexes epic), so the modifier has no runtime effect
15707        // — same accept-and-no-op shape as v7.37.16.5 DETACH
15708        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15709        // VIEW CONCURRENTLY.
15710        let mut concurrently = false;
15711        if matches!(
15712            self.peek(),
15713            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15714        ) {
15715            self.advance();
15716            concurrently = true;
15717        }
15718        let if_not_exists = self.consume_if_not_exists();
15719        // v7.39 (read01 round 93) — the index name is optional (PG since
15720        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15721        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15722        // was given; leave it empty and the engine derives a PG-style
15723        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15724        let name = if matches!(self.peek(), Token::On) {
15725            String::new()
15726        } else {
15727            self.expect_ident_like()?
15728        };
15729        if !matches!(self.peek(), Token::On) {
15730            return Err(self.err(format!(
15731                "expected ON after CREATE INDEX <name>, got {:?}",
15732                self.peek()
15733            )));
15734        }
15735        self.advance();
15736        let table = self.expect_ident_like()?;
15737        // Optional `USING <method>` — only recognised method in v2.0 is
15738        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15739        // ident `using` (we don't promote it to a reserved keyword
15740        // because it isn't reserved anywhere else in our SQL surface).
15741        let mut method_name: Option<String> = None;
15742        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15743            self.advance();
15744            let m = self.expect_ident_like()?;
15745            method_name = Some(m.to_ascii_lowercase());
15746            match m.to_ascii_lowercase().as_str() {
15747                "hnsw" => IndexMethod::Hnsw,
15748                "btree" => IndexMethod::BTree,
15749                "brin" => IndexMethod::Brin,
15750                // v7.12.3 — real GIN inverted index over `tsvector`.
15751                // v7.9.26b's `USING gin` → BTree silent fallback is
15752                // gone; the engine validates that the indexed column
15753                // is `tsvector` at CREATE INDEX time.
15754                "gin" => IndexMethod::Gin,
15755                // v7.9.26b — PG `pg_dump` emits `USING gist` /
15756                // `USING spgist` / `USING hash` for their built-in
15757                // AMs that SPG doesn't have a matching
15758                // implementation for; degrade to BTree on the
15759                // leading column so the schema loads + the index
15760                // catalogue stays consistent. Operator pays the
15761                // planner cost only for the queries that would have
15762                // used the specialised AM.
15763                "gist" | "spgist" | "hash" => IndexMethod::BTree,
15764                // v7.11.3 — pgvector ships both `ivfflat` and
15765                // `hnsw`. Customers shouldn't have to choose
15766                // their on-disk index method based on what SPG
15767                // implements; accept `ivfflat` as a synonym for
15768                // `hnsw` so PG schemas using either method drop
15769                // in. The vector distance op (`<->` / `<#>` /
15770                // `<=>`) at query time still picks the metric.
15771                "ivfflat" => IndexMethod::Hnsw,
15772                other => {
15773                    return Err(self.err(alloc::format!(
15774                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15775                    )));
15776                }
15777            }
15778        } else {
15779            IndexMethod::BTree
15780        };
15781        if !matches!(self.peek(), Token::LParen) {
15782            return Err(self.err(format!(
15783                "expected '(' before indexed column, got {:?}",
15784                self.peek()
15785            )));
15786        }
15787        self.advance();
15788        // v6.8.2 — accept either a bare column ident (legacy) or
15789        // an expression `fn(col, …)` for expression indexes.
15790        // Distinguish by peeking the token *after* the current
15791        // ident: `ident )` is the legacy column-only path;
15792        // anything else triggers the Pratt expression parser.
15793        // (`advance()` uses `mem::replace` to nil out the current
15794        // slot, so we can't save+rewind cleanly — peek-ahead via
15795        // direct index avoids the mutation.)
15796        let mut opclass: Option<String> = None;
15797        let mut key_collation: Option<String> = None;
15798        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
15799            // Single column with `)` immediately after — fast path.
15800            // v7.9.29 — also: bare column followed by `,` (the
15801            // multi-column form `(a, b, c)`). Without this branch
15802            // the leading ident gets pulled into `parse_expr`
15803            // which then sets `expression = Some(Column(a))` and
15804            // breaks Display round-trip on the multi-column shape.
15805            Token::Ident(s) | Token::QuotedIdent(s)
15806                if matches!(
15807                    self.tokens.get(self.pos + 1),
15808                    Some(Token::RParen | Token::Comma)
15809                ) =>
15810            {
15811                self.advance();
15812                (s, None)
15813            }
15814            // v7.9.22 — single column followed by a pgvector
15815            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
15816            // v7.15.0 — capture the opclass instead of discarding
15817            // it so the engine can dispatch (e.g. `gin_trgm_ops`
15818            // → real trigram-shingle GIN over a TEXT column).
15819            // Vector/HNSW opclasses still take their distance
15820            // metric from the query operator (`<->` / `<#>` /
15821            // `<=>`), so for those callers the opclass stays
15822            // informational.
15823            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
15824            // opclass: `(embedding public.vector_cosine_ops)`. Strip
15825            // the schema and dispatch on the bare opclass, the same
15826            // treatment table/type names get.
15827            Token::Ident(s) | Token::QuotedIdent(s)
15828                if matches!(
15829                    self.tokens.get(self.pos + 1),
15830                    Some(Token::Ident(_) | Token::QuotedIdent(_))
15831                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
15832                    && matches!(
15833                        self.tokens.get(self.pos + 3),
15834                        Some(Token::Ident(op) | Token::QuotedIdent(op))
15835                            if is_vector_opclass_name(op)
15836                    ) =>
15837            {
15838                self.advance(); // column name
15839                self.advance(); // schema qualifier
15840                self.advance(); // dot
15841                let op_tok = self.advance();
15842                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15843                    opclass = Some(op.to_ascii_lowercase());
15844                }
15845                (s, None)
15846            }
15847            // r1038 — an operator class is recognised by its POSITION, not
15848            // by a list of names. It used to be `is_vector_opclass_name`,
15849            // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
15850            // sentori's migration wrote — was a syntax error while
15851            // `USING gin (doc)` parsed. Anything sitting between a column
15852            // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
15853            // two bare identifiers in a row are not valid there otherwise.
15854            Token::Ident(s) | Token::QuotedIdent(s)
15855                if matches!(
15856                    self.tokens.get(self.pos + 1),
15857                    Some(Token::Ident(op) | Token::QuotedIdent(op))
15858                        if is_vector_opclass_name(op) || Self::opclass_position_follows(
15859                            self.tokens.get(self.pos + 2)
15860                        )
15861                ) =>
15862            {
15863                self.advance(); // column name
15864                // Capture the opclass token, lower-cased for
15865                // case-insensitive engine dispatch.
15866                let op_tok = self.advance();
15867                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15868                    opclass = Some(op.to_ascii_lowercase());
15869                }
15870                (s, None)
15871            }
15872            Token::Ident(_) | Token::QuotedIdent(_) => {
15873                // v7.39 (round 538) — an explicit COLLATE on the key,
15874                // read by LOOKAHEAD because `parse_expr` absorbs the
15875                // clause as a no-op (SPG orders text by bytes, which is
15876                // the C collation, so it changes nothing to honour). PG
15877                // still PRINTS it: an explicitly written `"C"` and the
15878                // collation a column inherits are different collation
15879                // OBJECTS even where they sort identically, which is why
15880                // `(a COLLATE "C")` shows on a C-collation database too.
15881                if matches!(
15882                    self.tokens.get(self.pos + 1),
15883                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
15884                ) {
15885                    key_collation = match self.tokens.get(self.pos + 2) {
15886                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
15887                            Some(n.clone())
15888                        }
15889                        _ => None,
15890                    };
15891                }
15892                let key_expr = self.parse_expr(0)?;
15893                let primary = extract_first_column(&key_expr).ok_or_else(|| {
15894                    self.err("expression index key must reference at least one column".into())
15895                })?;
15896                (primary, Some(key_expr))
15897            }
15898            // v7.37.43-T4 — parenthesised expression index key
15899            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
15900            // PG's CREATE INDEX requires the expression to be in
15901            // its own parens to disambiguate function calls from
15902            // column lists, so this `LParen` is the inner open-paren
15903            // of an expression key. parse_expr handles the recursive
15904            // descent and consumes the matching `RParen`.
15905            Token::LParen => {
15906                let key_expr = self.parse_expr(0)?;
15907                let primary = extract_first_column(&key_expr).ok_or_else(|| {
15908                    self.err("expression index key must reference at least one column".into())
15909                })?;
15910                (primary, Some(key_expr))
15911            }
15912            other => {
15913                return Err(self.err(format!(
15914                    "expected column ident or expression, got {other:?}"
15915                )));
15916            }
15917        };
15918        // v7.9.14 — accept extra comma-separated columns inside
15919        // the index key parens (`CREATE INDEX … (a, b, c)`).
15920        // mailrs F2. Each extra column may carry an optional
15921        // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
15922        // — parsed and discarded; SPG doesn't honour direction
15923        // on a BTree index today (column ordering is intrinsic
15924        // to the storage). v7.10 will widen to genuine composite
15925        // index keys.
15926        let mut extra_columns: Vec<String> = Vec::new();
15927        // The leading column may also have ASC/DESC after it — and that
15928        // one is the column SPG indexes, so its clause is kept.
15929        let key_order = self.consume_optional_index_column_qualifiers();
15930        while matches!(self.peek(), Token::Comma) {
15931            self.advance();
15932            let extra = self.expect_ident_like()?;
15933            let _ = self.consume_optional_index_column_qualifiers();
15934            extra_columns.push(extra);
15935        }
15936        if !matches!(self.peek(), Token::RParen) {
15937            return Err(self.err(format!(
15938                "expected ')' after indexed column / expression, got {:?}",
15939                self.peek()
15940            )));
15941        }
15942        self.advance();
15943        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
15944        // index-only-scan annotation. Bare ident (not a reserved
15945        // keyword) so we test by case-insensitive string match.
15946        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
15947        {
15948            self.advance();
15949            if !matches!(self.peek(), Token::LParen) {
15950                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
15951            }
15952            self.advance();
15953            let mut cols = Vec::new();
15954            loop {
15955                cols.push(self.expect_ident_like()?);
15956                match self.peek() {
15957                    Token::Comma => {
15958                        self.advance();
15959                    }
15960                    Token::RParen => {
15961                        self.advance();
15962                        break;
15963                    }
15964                    other => {
15965                        return Err(self.err(format!(
15966                            "expected ',' or ')' in INCLUDE list, got {other:?}"
15967                        )));
15968                    }
15969                }
15970            }
15971            cols
15972        } else {
15973            Vec::new()
15974        };
15975        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
15976        // storage parameters. pgvector emits `WITH (lists = N)` for
15977        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
15978        // SPG's HNSW picks its own parameters today (tunable via
15979        // env vars), so the WITH clause is informational and dropped.
15980        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15981            self.advance();
15982            if !matches!(self.peek(), Token::LParen) {
15983                return Err(self.err(format!(
15984                    "expected '(' after WITH in CREATE INDEX, got {:?}",
15985                    self.peek()
15986                )));
15987            }
15988            self.advance();
15989            loop {
15990                if matches!(self.peek(), Token::RParen) {
15991                    self.advance();
15992                    break;
15993                }
15994                // Drain `key = value` or bare `key` tokens.
15995                let _ = self.advance(); // key
15996                if matches!(self.peek(), Token::Eq) {
15997                    self.advance();
15998                    let _ = self.advance(); // value (int / string / ident)
15999                }
16000                match self.peek() {
16001                    Token::Comma => {
16002                        self.advance();
16003                    }
16004                    Token::RParen => {
16005                        self.advance();
16006                        break;
16007                    }
16008                    other => {
16009                        return Err(self.err(format!(
16010                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
16011                        )));
16012                    }
16013                }
16014            }
16015        }
16016        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16017        // which sits between the key list and the WHERE clause.
16018        let mut nulls_not_distinct = false;
16019        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16020            let n1 = self.tokens.get(self.pos + 1);
16021            let n2 = self.tokens.get(self.pos + 2);
16022            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16023                self.advance(); // NULLS
16024                self.advance(); // NOT
16025                self.advance(); // DISTINCT
16026                nulls_not_distinct = true;
16027            } else if matches!(n1, Some(Token::Distinct)) {
16028                self.advance(); // NULLS
16029                self.advance(); // DISTINCT
16030            }
16031        }
16032        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16033        let partial_predicate = if matches!(self.peek(), Token::Where) {
16034            self.advance();
16035            Some(self.parse_expr(0)?)
16036        } else {
16037            None
16038        };
16039        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16040        // sense: uniqueness over an ANN structure has no clean
16041        // semantics. Reject early. (BRIN UNIQUE is similarly
16042        // meaningless — block both.)
16043        if is_unique && !matches!(method, IndexMethod::BTree) {
16044            return Err(self.err(alloc::format!(
16045                "UNIQUE is only supported on BTree indexes, got USING {:?}",
16046                method
16047            )));
16048        }
16049        Ok(Statement::CreateIndex(CreateIndexStatement {
16050            concurrently,
16051            name,
16052            key_order,
16053            key_collation,
16054            table,
16055            column,
16056            nulls_not_distinct,
16057            method,
16058            if_not_exists,
16059            included_columns,
16060            partial_predicate,
16061            extra_columns: extra_columns.clone(),
16062            expression,
16063            is_unique,
16064            opclass,
16065            method_name,
16066        }))
16067    }
16068
16069    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16070    /// column-level `REFERENCES ...` clause. The trailing FK is
16071    /// normalised into table-level shape (single-element columns +
16072    /// parent_columns) so the engine sees one uniform constraint list.
16073    fn parse_column_def_with_fk(
16074        &mut self,
16075    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16076        let col = self.parse_column_def()?;
16077        // v7.39 (round 308, V29) — an explicitly named inline FK:
16078        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16079        // loop leaves this spelling intact precisely so the name can be
16080        // kept here; PG reports it in violation messages and matches it
16081        // in `SET CONSTRAINTS`.
16082        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16083        {
16084            self.advance();
16085            Some(self.expect_ident_like()?)
16086        } else {
16087            None
16088        };
16089        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16090        let inline_references = matches!(
16091            self.peek(),
16092            Token::Ident(s) if s.eq_ignore_ascii_case("references")
16093        );
16094        if !inline_references {
16095            return Ok((col, None));
16096        }
16097        let (
16098            parent_table,
16099            parent_columns,
16100            on_delete,
16101            on_update,
16102            match_type,
16103            deferrable,
16104            initially_deferred,
16105        ) = self.parse_references_tail(1)?;
16106        let fk = ForeignKeyConstraint {
16107            name: declared_name,
16108            columns: vec![col.name.clone()],
16109            parent_table,
16110            parent_columns,
16111            on_delete,
16112            on_update,
16113            match_type,
16114            deferrable,
16115            initially_deferred,
16116        };
16117        Ok((col, Some(fk)))
16118    }
16119
16120    /// v7.13.0 — parse a column type (consuming the type ident and
16121    /// any trailing parameters / `[]`), without surrounding column
16122    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16123    /// Returns the resolved `ColumnTypeName` plus implied
16124    /// `(auto_increment, not_null)` flags from PG SERIAL family
16125    /// shorthands — callers that don't expect those (ALTER COLUMN
16126    /// TYPE) can discard them.
16127    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16128        let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16129        Ok(ty)
16130    }
16131
16132    #[allow(clippy::type_complexity)]
16133    fn parse_type_with_implied_flags(
16134        &mut self,
16135    ) -> Result<
16136        (
16137            ColumnTypeName,
16138            bool,
16139            bool,
16140            Option<String>,
16141            Collation,
16142            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16143            bool,
16144            // v7.39 (round 676) — the collation NAME as written, which the
16145            // `Collation` enum above cannot carry.
16146            Option<String>,
16147            bool,
16148            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16149            // list captured at type-parse time. None for all
16150            // non-ENUM types.
16151            Option<Vec<String>>,
16152            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16153            // list. Distinct from ENUM (subset semantics).
16154            Option<Vec<String>>,
16155            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16156            // width, lost when the type collapses to SmallInt / Int.
16157            Option<MysqlIntWidth>,
16158            // v7.39 (round 424) — declared fractional-seconds precision of a
16159            // MySQL temporal column (bare spelling = 0). None under PG.
16160            Option<u8>,
16161        ),
16162        ParseError,
16163    > {
16164        let mut ty_ident = match self.advance() {
16165            Token::Ident(s) => s,
16166            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16167            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16168            // '<span>'` literal grammar. As a column type it lands
16169            // here directly; downstream resolution still uses the
16170            // canonical lowercase string.
16171            Token::Interval => "interval".to_string(),
16172            other => {
16173                return Err(ParseError {
16174                    message: format!("expected column type, got {other:?}"),
16175                    token_pos: self.consumed_pos(),
16176                });
16177            }
16178        };
16179        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16180        // pg_dump qualifies extension types (`public.vector(1024)`).
16181        // SPG is single-namespace; drop the schema and resolve the
16182        // bare type — same treatment table names already get.
16183        while matches!(self.peek(), Token::Dot) {
16184            self.advance();
16185            ty_ident = self.expect_ident_like()?;
16186        }
16187        let mut implied_auto_increment = false;
16188        let mut implied_not_null = false;
16189        let mut user_type_ref: Option<String> = None;
16190        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16191        // value list, captured here and bubbled up through the
16192        // ColumnDef so the engine can attach it to the column
16193        // schema (and validate INSERT cells against it).
16194        let mut inline_enum_variants: Option<Vec<String>> = None;
16195        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16196        let mut inline_set_variants: Option<Vec<String>> = None;
16197        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16198        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16199        // collapses to SmallInt / Int. Only under the MySQL dialect.
16200        let mut mysql_int_width: Option<MysqlIntWidth> = None;
16201        // v7.39 (round 424) — the declared fractional-seconds precision of a
16202        // MySQL temporal column. Set by the temporal arms below; stays None
16203        // for PG (whose temporal columns keep full microseconds).
16204        let mut mysql_fsp: Option<u8> = None;
16205        let mut ty = match ty_ident.as_str() {
16206            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16207            "smallserial" | "serial2" => {
16208                implied_auto_increment = true;
16209                implied_not_null = true;
16210                ColumnTypeName::SmallInt
16211            }
16212            "serial" | "serial4" => {
16213                implied_auto_increment = true;
16214                implied_not_null = true;
16215                ColumnTypeName::Int
16216            }
16217            "bigserial" | "serial8" => {
16218                implied_auto_increment = true;
16219                implied_not_null = true;
16220                ColumnTypeName::BigInt
16221            }
16222            // MySQL flavours we accept by aliasing to the closest SPG
16223            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16224            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16225            // 24-bit) → INT. UNSIGNED modifiers are consumed below
16226            // without semantic effect.
16227            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16228            // PG's internal type names; pg_dump and hand-written PG schemas
16229            // use them interchangeably with smallint / int / bigint (the cast
16230            // path already accepted them, only the column grammar didn't).
16231            "smallint" | "int2" => {
16232                // v7.14.0 — MySQL display-width on integers
16233                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16234                // parenthesised number is purely cosmetic — it
16235                // doesn't change storage. Accept + discard.
16236                self.consume_optional_paren_size();
16237                ColumnTypeName::SmallInt
16238            }
16239            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16240            // canonical encoding for BOOLEAN. Every MySQL driver
16241            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16242            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16243            // 4.3 SPG classified TINYINT(1) as SmallInt, which
16244            // gave the customer i16-shaped values where the app
16245            // expected bool — a Tier-A silent type drift on
16246            // mysqldump restores. Now: `TINYINT(1)` → Bool;
16247            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16248            // stay SmallInt (the legacy width-agnostic path).
16249            "tinyint" => {
16250                let width = self.peek_optional_paren_size_value();
16251                self.consume_optional_paren_size();
16252                if width == Some(1) {
16253                    ColumnTypeName::Bool
16254                } else {
16255                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16256                    // lost width so the write path can enforce -128..127.
16257                    if self.mysql_dialect {
16258                        mysql_int_width = Some(MysqlIntWidth::Tiny);
16259                    }
16260                    ColumnTypeName::SmallInt
16261                }
16262            }
16263            "mediumint" => {
16264                self.consume_optional_paren_size();
16265                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16266                if self.mysql_dialect {
16267                    mysql_int_width = Some(MysqlIntWidth::Medium);
16268                }
16269                ColumnTypeName::Int
16270            }
16271            "int" | "integer" | "int4" => {
16272                self.consume_optional_paren_size();
16273                ColumnTypeName::Int
16274            }
16275            "bigint" | "int8" => {
16276                self.consume_optional_paren_size();
16277                ColumnTypeName::BigInt
16278            }
16279            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16280            // (mailrs round-5 G6). Consume the optional `PRECISION`
16281            // tail when the type keyword was `double` / `DOUBLE`.
16282            //
16283            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16284            // FLOAT". `FLOAT(p)` picks the width the way PG does:
16285            // p in 1..=24 is real, 25..=53 is double precision, and
16286            // anything else is an error.
16287            "float" | "double" | "real" => {
16288                if ty_ident.eq_ignore_ascii_case("double")
16289                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16290                {
16291                    self.advance();
16292                }
16293                if ty_ident.eq_ignore_ascii_case("real") {
16294                    // v7.39 (round 274) — the two dialects genuinely
16295                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16296                    // synonym for DOUBLE (8-byte). Round 269 made REAL
16297                    // 32-bit globally and thereby narrowed the stored
16298                    // precision of every MySQL REAL column.
16299                    if self.mysql_dialect {
16300                        ColumnTypeName::Float
16301                    } else {
16302                        ColumnTypeName::Real
16303                    }
16304                } else if ty_ident.eq_ignore_ascii_case("float")
16305                    && self.mysql_dialect
16306                    && matches!(self.peek(), Token::LParen)
16307                    && self.peek_paren_has_comma()
16308                {
16309                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16310                    // display form (`FLOAT(10,2)`), which PG has no
16311                    // equivalent of. It was `syntax error at or near ","`,
16312                    // so the whole CREATE failed. The digits are a display
16313                    // hint only; SPG stores the full double.
16314                    self.consume_optional_paren_size();
16315                    ColumnTypeName::Float
16316                } else if ty_ident.eq_ignore_ascii_case("float")
16317                    && matches!(self.peek(), Token::LParen)
16318                {
16319                    // PG words the two bounds differently, and
16320                    // parse_paren_size already rejects a zero.
16321                    let p = self.parse_paren_size("FLOAT")?;
16322                    if p > 53 {
16323                        return Err(self.err(String::from(
16324                            "precision for type float must be less than 54 bits",
16325                        )));
16326                    }
16327                    if p <= 24 {
16328                        ColumnTypeName::Real
16329                    } else {
16330                        ColumnTypeName::Float
16331                    }
16332                } else {
16333                    ColumnTypeName::Float
16334                }
16335            }
16336            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16337            "float4" => ColumnTypeName::Real,
16338            "float8" => ColumnTypeName::Float,
16339            "text" => ColumnTypeName::Text,
16340            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16341            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16342            // real MySQL schema and NONE of them existed: the CREATE
16343            // failed outright with `type "blob" does not exist`, so the
16344            // table was never made. The sizes differ only in MySQL's
16345            // maximum length, which SPG does not cap, so they collapse
16346            // onto TEXT and BYTEA the way the unsized spellings do.
16347            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16348            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16349            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16350            // enforce, consumed so the declaration parses.
16351            "varbinary" | "binary" => {
16352                self.consume_optional_paren_size();
16353                ColumnTypeName::Bytes
16354            }
16355            "name" => ColumnTypeName::Name,
16356            "xid" => ColumnTypeName::Xid,
16357            "oid" => ColumnTypeName::Oid,
16358            "xid8" => ColumnTypeName::Xid8,
16359            "bool" | "boolean" => ColumnTypeName::Bool,
16360            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16361            // an unbounded `character varying`, which the arm below has always
16362            // read as text. Only the short spelling demanded a length, so
16363            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16364            // there is — failed on `VARCHAR type requires (N)` while the long
16365            // spelling of the same thing was accepted. The same asymmetry
16366            // round 613 closed on the CAST side, here on the DDL side.
16367            "varchar" => {
16368                if matches!(self.peek(), Token::LParen) {
16369                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16370                } else {
16371                    ColumnTypeName::Text
16372                }
16373            }
16374            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16375            // `character` below (SQL standard).
16376            "char" => {
16377                if matches!(self.peek(), Token::LParen) {
16378                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16379                } else {
16380                    ColumnTypeName::Char(1)
16381                }
16382            }
16383            // pg_dump's canonical spellings: `character varying(n)` = varchar,
16384            // `character(n)` = char, bare `character` = char(1). Unbounded
16385            // `character varying` maps to text.
16386            "character" => {
16387                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16388                    self.advance();
16389                    if matches!(self.peek(), Token::LParen) {
16390                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16391                    } else {
16392                        ColumnTypeName::Text
16393                    }
16394                } else if matches!(self.peek(), Token::LParen) {
16395                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16396                } else {
16397                    ColumnTypeName::Char(1)
16398                }
16399            }
16400            "vector" => {
16401                let dim = self.parse_paren_size("VECTOR")?;
16402                let encoding = self.parse_optional_vector_encoding()?;
16403                ColumnTypeName::Vector { dim, encoding }
16404            }
16405            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16406            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16407            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16408            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16409            // DECIMAL(10,2))` — how nearly every money column is written,
16410            // in either dialect — was a syntax error and the table was
16411            // never created. `FIXED` is MySQL's alias alone, so it is
16412            // taken only in that dialect.
16413            "numeric" | "decimal" | "dec" => {
16414                let (precision, scale) = self.parse_optional_numeric_params()?;
16415                ColumnTypeName::Numeric(precision, scale)
16416            }
16417            "fixed" if self.mysql_dialect => {
16418                let (precision, scale) = self.parse_optional_numeric_params()?;
16419                ColumnTypeName::Numeric(precision, scale)
16420            }
16421            "date" => ColumnTypeName::Date,
16422            // MySQL's `DATETIME` is the same domain as standard
16423            // `TIMESTAMP` — accept both spellings.
16424            "timestamp" | "datetime" => {
16425                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16426                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16427                // TIME ZONE` clause, so consume it first.
16428                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16429                // (it truncates on write and pads on render), so capture it;
16430                // a bare spelling means precision 0 there. PG stores µs always
16431                // and keeps `None`.
16432                let n = self.take_optional_paren_size();
16433                if self.mysql_dialect {
16434                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16435                }
16436                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16437                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16438                // the full form. SPG canonicalises:
16439                //   - WITH TIME ZONE    → Timestamptz
16440                //   - WITHOUT TIME ZONE → Timestamp
16441                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16442                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16443                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16444                {
16445                    self.advance(); // WITH
16446                    self.advance(); // TIME
16447                    self.advance(); // ZONE
16448                    ColumnTypeName::Timestamptz
16449                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16450                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16451                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16452                {
16453                    self.advance(); // WITHOUT
16454                    self.advance(); // TIME
16455                    self.advance(); // ZONE
16456                    ColumnTypeName::Timestamp
16457                } else {
16458                    // A second `(precision)` cannot legally follow, but the
16459                    // old grammar tolerated it; keep that tolerance.
16460                    self.consume_optional_paren_size();
16461                    ColumnTypeName::Timestamp
16462                }
16463            }
16464            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16465            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16466            // only PG-wire OID differs.
16467            "timestamptz" => {
16468                self.consume_optional_paren_size();
16469                ColumnTypeName::Timestamptz
16470            }
16471            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16472            // validation. We accept the JSONB spelling too because
16473            // most PG clients default to it; SPG doesn't distinguish
16474            // the two (no path-operator perf advantage to model).
16475            "json" => ColumnTypeName::Json,
16476            "jsonb" => ColumnTypeName::Jsonb,
16477            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16478            // surface here. Same storage shape; mapping happens at
16479            // the engine side via the ColumnTypeName → DataType
16480            // resolver. Literal forms are handled at coerce_value
16481            // time so the lexer stays untouched.
16482            "bytea" | "bytes" => ColumnTypeName::Bytes,
16483            // v7.17.0 Phase 7 — PG network address types
16484            // v7.17.0 had a Text-backed fallback here for
16485            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16486            // each to a first-class type; the keywords are
16487            // bound below in the ζ-A block.
16488            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16489            // The actual `to_tsvector` / `@@` / `ts_rank` surface
16490            // arrives in v7.12.1+; the type itself loads here so
16491            // mailrs's `scripts/init-schema.sql` runs unmodified.
16492            "tsvector" => ColumnTypeName::TsVector,
16493            "tsquery" => ColumnTypeName::TsQuery,
16494            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16495            // surface for Django / Rails / Hibernate's default
16496            // PK pattern.
16497            "uuid" => ColumnTypeName::Uuid,
16498            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16499            // Storage = three-field {months, days, micros}, catalog
16500            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16501            // line `INTERVAL` was parser-rejected at CREATE TABLE.
16502            "interval" => {
16503                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16504                // SECOND` and an optional `(p)` precision. SPG stores the full
16505                // {months,days,micros}; consume + ignore the qualifier/precision.
16506                while matches!(self.peek(), Token::To)
16507                    || matches!(self.peek(), Token::Ident(s) if matches!(
16508                        s.to_ascii_lowercase().as_str(),
16509                        "year" | "month" | "day" | "hour" | "minute" | "second"
16510                    ))
16511                {
16512                    self.advance();
16513                }
16514                self.consume_optional_paren_size();
16515                ColumnTypeName::Interval
16516            }
16517            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16518            // i64 microseconds since 00:00:00. Wire OID 1083.
16519            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16520            "time" => {
16521                // v7.39 (round 424) — MySQL TIME carries a semantic
16522                // fractional-seconds precision, bare meaning 0.
16523                let n = self.take_optional_paren_size();
16524                if self.mysql_dialect {
16525                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16526                }
16527                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16528                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16529                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16530                {
16531                    self.advance();
16532                    self.advance();
16533                    self.advance();
16534                    ColumnTypeName::TimeTz
16535                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16536                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16537                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16538                {
16539                    self.advance();
16540                    self.advance();
16541                    self.advance();
16542                    ColumnTypeName::Time
16543                } else {
16544                    ColumnTypeName::Time
16545                }
16546            }
16547            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16548            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16549            "year" => ColumnTypeName::Year,
16550            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16551            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16552            "timetz" => ColumnTypeName::TimeTz,
16553            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16554            // Wire OID 790.
16555            "money" => ColumnTypeName::Money,
16556            // v7.17.0 Phase 3.P0-38 — PG range types.
16557            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16558            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16559            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16560            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16561            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16562            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16563            // v7.37.5 δ — PG 14+ multirange keywords.
16564            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16565            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16566            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16567            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16568            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16569            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16570            // v7.37.5 ε — PG geometry scalar keywords.
16571            "point" => ColumnTypeName::Point,
16572            "lseg" => ColumnTypeName::Lseg,
16573            "path" => ColumnTypeName::Path,
16574            "box" => ColumnTypeName::PgBox,
16575            "polygon" => ColumnTypeName::Polygon,
16576            "line" => ColumnTypeName::Line,
16577            "circle" => ColumnTypeName::Circle,
16578            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16579            "inet" => ColumnTypeName::Inet,
16580            "cidr" => ColumnTypeName::Cidr,
16581            "macaddr" => ColumnTypeName::Macaddr,
16582            "macaddr8" => ColumnTypeName::Macaddr8,
16583            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16584            // width in the value, so the optional `(N)` typmod is accepted and
16585            // ignored (the column stores whatever width it's given).
16586            "bit" => {
16587                let varying = matches!(
16588                    self.peek(),
16589                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16590                );
16591                if varying {
16592                    self.advance();
16593                }
16594                // v7.39 (round 281) — the length used to be parsed and
16595                // dropped, so `bit(3)` accepted a five-bit string.
16596                let n = if matches!(self.peek(), Token::LParen) {
16597                    self.parse_paren_size("BIT")?
16598                } else {
16599                    0
16600                };
16601                if varying {
16602                    ColumnTypeName::BitVarying(n)
16603                } else {
16604                    ColumnTypeName::Bit(n)
16605                }
16606            }
16607            "varbit" => {
16608                let n = if matches!(self.peek(), Token::LParen) {
16609                    self.parse_paren_size("VARBIT")?
16610                } else {
16611                    0
16612                };
16613                ColumnTypeName::BitVarying(n)
16614            }
16615            "xml" => ColumnTypeName::Xml,
16616            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16617            "hstore" => ColumnTypeName::Hstore,
16618            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16619            // `ENUM('a','b','c')`. Storage is TEXT; the value
16620            // list lands on `inline_enum_variants` for the
16621            // engine to validate INSERT cells against. Empty
16622            // value list is a parse error (matches MySQL).
16623            "enum" => {
16624                // Expect the opening `(`.
16625                if !matches!(self.peek(), Token::LParen) {
16626                    return Err(self.err(alloc::format!(
16627                        "expected '(' after ENUM, got {:?}",
16628                        self.peek()
16629                    )));
16630                }
16631                self.advance();
16632                let mut variants: Vec<String> = Vec::new();
16633                loop {
16634                    match self.advance() {
16635                        Token::String(s) => variants.push(s),
16636                        other => {
16637                            return Err(self.err(alloc::format!(
16638                                "ENUM(...) expects string literal variants, got {other:?}"
16639                            )));
16640                        }
16641                    }
16642                    match self.peek() {
16643                        Token::Comma => {
16644                            self.advance();
16645                            continue;
16646                        }
16647                        Token::RParen => {
16648                            self.advance();
16649                            break;
16650                        }
16651                        other => {
16652                            return Err(self.err(alloc::format!(
16653                                "expected ',' or ')' in ENUM(...), got {other:?}"
16654                            )));
16655                        }
16656                    }
16657                }
16658                if variants.is_empty() {
16659                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
16660                }
16661                inline_enum_variants = Some(variants);
16662                // Storage is plain TEXT; the variant list lives on
16663                // the ColumnSchema side.
16664                ColumnTypeName::Text
16665            }
16666            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16667            // `SET('a','b','c')`. Same parse shape as ENUM;
16668            // semantics differ (subset rather than pick-one).
16669            "set" => {
16670                if !matches!(self.peek(), Token::LParen) {
16671                    return Err(self.err(alloc::format!(
16672                        "expected '(' after SET, got {:?}",
16673                        self.peek()
16674                    )));
16675                }
16676                self.advance();
16677                let mut variants: Vec<String> = Vec::new();
16678                loop {
16679                    match self.advance() {
16680                        Token::String(s) => variants.push(s),
16681                        other => {
16682                            return Err(self.err(alloc::format!(
16683                                "SET(...) expects string literal variants, got {other:?}"
16684                            )));
16685                        }
16686                    }
16687                    match self.peek() {
16688                        Token::Comma => {
16689                            self.advance();
16690                            continue;
16691                        }
16692                        Token::RParen => {
16693                            self.advance();
16694                            break;
16695                        }
16696                        other => {
16697                            return Err(self.err(alloc::format!(
16698                                "expected ',' or ')' in SET(...), got {other:?}"
16699                            )));
16700                        }
16701                    }
16702                }
16703                if variants.is_empty() {
16704                    return Err(self.err("SET(...) must declare at least one variant".into()));
16705                }
16706                inline_set_variants = Some(variants);
16707                ColumnTypeName::Text
16708            }
16709            _other => {
16710                // v7.17.0 Phase 1.4 — unknown ident → defer
16711                // resolution to the engine. Stored as Text in
16712                // ColumnTypeName + the original name carried as
16713                // `user_type_ref` so CREATE TABLE can look up
16714                // user-defined enum / domain types.
16715                user_type_ref = Some(ty_ident.clone());
16716                ColumnTypeName::Text
16717            }
16718        };
16719        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16720        // right after the type keyword. Pre-4.4 SPG consumed +
16721        // discarded the keyword, leaving a customer column
16722        // declared `id INT UNSIGNED NOT NULL` silently accepting
16723        // negative values — a Tier-A correctness drift where
16724        // application invariants (auto-increment-IDs never
16725        // negative) silently broke on cutover. Now: capture as
16726        // a column flag, persist on the schema, enforce at
16727        // INSERT / UPDATE time.
16728        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16729        {
16730            self.advance();
16731            true
16732        } else {
16733            false
16734        };
16735        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16736        // `<type> COLLATE <name>` post-fixes on text columns. SPG
16737        // stores text as UTF-8 always so CHARACTER SET is still a
16738        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16739        // name: it gets classified into a `Collation` variant the
16740        // engine consults at WHERE-eval time. PG `default` /
16741        // `pg_catalog.default` / `C` / `POSIX` collations all
16742        // resolve to `Binary` (the prior behaviour); `_ci` /
16743        // `case_insensitive` / `nocase` shift to CaseInsensitive.
16744        // The schema-qualifier form (`pg_catalog.default`) lexes
16745        // as `Ident '.' Ident` — peek for the `.` and consume both
16746        // halves so it's treated as one collation name. PG's
16747        // `IDENT.IDENT` collation form (which can appear here) is
16748        // resolved by Collation::from_collation_name on the bare
16749        // identifier after the dot.
16750        let mut collation = Collation::Binary;
16751        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16752        // clause was written. The engine needs this to tell an explicit
16753        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16754        // clause at all: both resolve to `Collation::Binary`, but under the
16755        // MySQL dialect the latter takes the folding default collation.
16756        let mut collation_explicit = false;
16757        let mut collation_name: Option<alloc::string::String> = None;
16758        loop {
16759            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16760                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16761            {
16762                self.advance(); // CHARACTER
16763                self.advance(); // SET
16764                if matches!(
16765                    self.peek(),
16766                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16767                ) {
16768                    self.advance();
16769                }
16770                continue;
16771            }
16772            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16773                self.advance(); // COLLATE
16774                // Accept Ident / QuotedIdent / String AND the
16775                // keyword-tokenised `Default` (PG `pg_catalog.default`
16776                // and bare `DEFAULT` collation names — `default` is a
16777                // reserved word so the lexer hands back Token::Default
16778                // not Token::Ident).
16779                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16780                    match this.peek().clone() {
16781                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16782                            this.advance();
16783                            Some(s)
16784                        }
16785                        Token::Default => {
16786                            this.advance();
16787                            Some(alloc::string::String::from("default"))
16788                        }
16789                        _ => None,
16790                    }
16791                };
16792                let raw = if let Some(head) = read_collation_atom(self) {
16793                    // Schema-qualified PG form: `pg_catalog.default`.
16794                    if matches!(self.peek(), Token::Dot) {
16795                        self.advance();
16796                        let tail = read_collation_atom(self).unwrap_or_default();
16797                        alloc::format!("{head}.{tail}")
16798                    } else {
16799                        head
16800                    }
16801                } else {
16802                    alloc::string::String::new()
16803                };
16804                if !raw.is_empty() {
16805                    collation_explicit = true;
16806                    // v7.39 (round 676) — keep the name too. The enum below
16807                    // folds C / POSIX / en_US / default into one value, and
16808                    // `pg_attribute.attcollation` has to tell them apart.
16809                    // The schema qualifier goes: PG's `pg_catalog.default`
16810                    // and a bare `default` name the same collation.
16811                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
16812                    // encoding suffix. Round 676 used `rsplit('.')` for
16813                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
16814                    // PG writes `pg_catalog.default` (qualifier) and
16815                    // `en_US.utf8` (locale + encoding) with the same
16816                    // separator. Only `pg_catalog.` is a qualifier, and it
16817                    // is the only one PG's own dumps emit.
16818                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
16819                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
16820                    collation_name = Some(alloc::string::String::from(bare));
16821                    let parsed = Collation::from_collation_name(&raw);
16822                    // Last COLLATE clause wins, but `Binary` from a
16823                    // bare keyword like `default` should not
16824                    // silently downgrade a stronger one set earlier
16825                    // on the same column. v7.17 only ships one
16826                    // non-Binary variant so a simple OR is enough.
16827                    if parsed != Collation::Binary {
16828                        collation = parsed;
16829                    }
16830                }
16831                continue;
16832            }
16833            break;
16834        }
16835        // v7.10.10 — postfix `[]` widens the base type to its array
16836        // type. PG accepts `TYPE[]` after any base type and so does
16837        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
16838        // all through; the old "only TEXT[]" note was stale).
16839        if matches!(self.peek(), Token::LBracket) {
16840            self.advance();
16841            if !matches!(self.peek(), Token::RBracket) {
16842                return Err(self.err(alloc::format!(
16843                    "TEXT[] takes no dimension; got {:?}",
16844                    self.peek()
16845                )));
16846            }
16847            self.advance();
16848            // v7.11.13 — widened to INT[] and BIGINT[] in addition
16849            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
16850            // still error here.
16851            ty = match ty {
16852                ColumnTypeName::Text => ColumnTypeName::TextArray,
16853                ColumnTypeName::Int => ColumnTypeName::IntArray,
16854                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
16855                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
16856                // `[]` grammar. Wire OID 1187.
16857                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
16858                // v7.37.5 γ — full PG array-of-scalar family.
16859                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
16860                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
16861                ColumnTypeName::Float => ColumnTypeName::FloatArray,
16862                // NUMERIC(p, s) loses its precision params at the
16863                // array level (matches PG: `NUMERIC[]` is untyped,
16864                // per-element precision flows through values).
16865                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
16866                ColumnTypeName::Date => ColumnTypeName::DateArray,
16867                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
16868                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
16869                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
16870                ColumnTypeName::Json => ColumnTypeName::JsonArray,
16871                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
16872                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
16873                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
16874                // the array level (matches PG semantics where the
16875                // element precision is per-row, not column-wide).
16876                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
16877                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
16878                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
16879                // follow-up.
16880                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
16881                other => {
16882                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
16883                }
16884            };
16885            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
16886            // for INT/TEXT/BIGINT. Anything else is an error.
16887            if matches!(self.peek(), Token::LBracket) {
16888                self.advance();
16889                if !matches!(self.peek(), Token::RBracket) {
16890                    return Err(self.err(alloc::format!(
16891                        "TYPE[][] second dimension takes no size; got {:?}",
16892                        self.peek()
16893                    )));
16894                }
16895                self.advance();
16896                ty = match ty {
16897                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
16898                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
16899                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
16900                    // v7.39 (read01 round 75) — bool[][].
16901                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
16902                    other => {
16903                        return Err(self.err(alloc::format!(
16904                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
16905                             TEXT[][] only; got {other:?}"
16906                        )));
16907                    }
16908                };
16909            }
16910        }
16911        Ok((
16912            ty,
16913            implied_auto_increment,
16914            implied_not_null,
16915            user_type_ref,
16916            collation,
16917            collation_explicit,
16918            collation_name,
16919            is_unsigned,
16920            inline_enum_variants,
16921            inline_set_variants,
16922            mysql_int_width,
16923            mysql_fsp,
16924        ))
16925    }
16926
16927    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
16928        // v7.20 — PG reserves the table-constraint keywords, so a
16929        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
16930        // malformed constraint clause (e.g. `UNIQUE a` missing its
16931        // parens), not a column named "unique". Since v7.17's
16932        // unknown-type leniency (`user_type_ref`) such a clause
16933        // would otherwise parse as a column with a user-defined
16934        // type — silently accepting invalid DDL. Quoted
16935        // identifiers ("unique" / `unique`) remain valid names.
16936        if let Token::Ident(s) = self.peek()
16937            && [
16938                "unique",
16939                "primary",
16940                "foreign",
16941                "constraint",
16942                "check",
16943                "references",
16944                "exclude",
16945            ]
16946            .iter()
16947            .any(|kw| s.eq_ignore_ascii_case(kw))
16948        {
16949            return Err(self.err(alloc::format!(
16950                "unexpected reserved keyword '{s}' at start of column definition \
16951                 (malformed table constraint?)"
16952            )));
16953        }
16954        let name = self.expect_ident_like()?;
16955        let (
16956            ty,
16957            implied_auto_increment,
16958            implied_not_null,
16959            user_type_ref,
16960            collation,
16961            collation_explicit,
16962            collation_name,
16963            is_unsigned,
16964            inline_enum_variants,
16965            inline_set_variants,
16966            mysql_int_width,
16967            mysql_fsp,
16968        ) = self.parse_type_with_implied_flags()?;
16969        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
16970        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
16971        // each at most once.
16972        let mut default: Option<Expr> = None;
16973        let mut nullable = !implied_not_null;
16974        let mut nullability_seen = implied_not_null;
16975        let mut auto_increment = implied_auto_increment;
16976        let mut is_primary_key = false;
16977        let mut is_unique = false;
16978        let mut unique_nulls_not_distinct = false;
16979        let mut constraint_deferrable = false;
16980        let mut constraint_initially_deferred = false;
16981        let mut check: Option<Expr> = None;
16982        let mut on_update_runtime: Option<Expr> = None;
16983        let mut generated_stored_expr: Option<Box<Expr>> = None;
16984        let mut identity_always = false;
16985        loop {
16986            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
16987            // not-null constraints by name and pg_dump emits them
16988            // inline: `id bigint CONSTRAINT contacts_id_not_null1
16989            // NOT NULL`. Accept and discard the name; whatever
16990            // constraint follows is parsed by the arms below.
16991            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16992                // v7.39 (round 308, V29) — a name on an inline
16993                // REFERENCES belongs to the FOREIGN KEY, and the caller
16994                // (`parse_column_def_with_fk`) is what builds it, so
16995                // leave the whole clause for it. Dropping the name here
16996                // is what made `CONSTRAINT fk_a REFERENCES …` come back
16997                // as the synthesised `c_pid_fkey` — which then could
16998                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
16999                // `advance()` takes tokens by `mem::replace`, so there
17000                // is no rewinding once consumed.
17001                if matches!(
17002                    self.tokens.get(self.pos + 2),
17003                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17004                ) {
17005                    break;
17006                }
17007                self.advance();
17008                let _name = self.expect_ident_like()?;
17009                continue;
17010            }
17011            // v7.39 (round 379) — MySQL's SHORT generated-column form
17012            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17013            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17014            // below), but hand-written schemas and app migrations use this.
17015            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17016            // SPG computes-and-stores either way, like the long form.
17017            if matches!(self.peek(), Token::As) {
17018                self.advance();
17019                if !matches!(self.peek(), Token::LParen) {
17020                    return Err(self.err(alloc::format!(
17021                        "expected '(' after AS in a generated column, got {:?}",
17022                        self.peek()
17023                    )));
17024                }
17025                self.advance();
17026                let expr = self.parse_expr(0)?;
17027                if !matches!(self.peek(), Token::RParen) {
17028                    return Err(self.err(alloc::format!(
17029                        "expected ')' after AS (<expr>), got {:?}",
17030                        self.peek()
17031                    )));
17032                }
17033                self.advance();
17034                if matches!(self.peek(), Token::Ident(s)
17035                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17036                {
17037                    self.advance();
17038                }
17039                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17040                continue;
17041            }
17042            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17043            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17044            // the modern replacement for SERIAL in hand-written
17045            // schemas). Both flavours map onto the auto-increment
17046            // machinery — SPG's serial semantics ≈ BY DEFAULT;
17047            // ALWAYS's reject-explicit-values nuance is documented
17048            // leniency. Generated EXPRESSION columns
17049            // (`AS (expr) STORED`) are not supported: error loudly
17050            // instead of silently storing NULLs.
17051            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17052                self.advance();
17053                let mut saw_generated_always = false;
17054                match self.peek().clone() {
17055                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17056                        self.advance();
17057                        saw_generated_always = true;
17058                    }
17059                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17060                        self.advance();
17061                        if !matches!(self.peek(), Token::Default) {
17062                            return Err(self.err(alloc::format!(
17063                                "expected DEFAULT after GENERATED BY, got {:?}",
17064                                self.peek()
17065                            )));
17066                        }
17067                        self.advance();
17068                    }
17069                    other => {
17070                        return Err(self.err(alloc::format!(
17071                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17072                        )));
17073                    }
17074                }
17075                if !matches!(self.peek(), Token::As) {
17076                    return Err(self.err(alloc::format!(
17077                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17078                        self.peek()
17079                    )));
17080                }
17081                self.advance();
17082                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17083                // ( <expr> ) STORED` stored computed-column. The
17084                // expression is captured for the engine to recompute
17085                // on every INSERT / UPDATE. v7.37.7 accepts the
17086                // STORED keyword only; PG also has VIRTUAL, which
17087                // v7.37.7 carves out (sentori only uses STORED).
17088                if matches!(self.peek(), Token::LParen) {
17089                    self.advance();
17090                    let expr = self.parse_expr(0)?;
17091                    if !matches!(self.peek(), Token::RParen) {
17092                        return Err(self.err(alloc::format!(
17093                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17094                            self.peek()
17095                        )));
17096                    }
17097                    self.advance();
17098                    let stored = match self.peek() {
17099                        Token::Ident(s) | Token::QuotedIdent(s)
17100                            if s.eq_ignore_ascii_case("stored") =>
17101                        {
17102                            self.advance();
17103                            true
17104                        }
17105                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17106                        // generated columns. SPG computes them on write and
17107                        // persists like STORED; the two are observably
17108                        // identical for query results (the value, recompute
17109                        // on base-column change, and NOT NULL enforcement all
17110                        // match), so a PG 18 schema/dump using VIRTUAL loads
17111                        // and behaves correctly. The compute-on-read storage
17112                        // saving is an invisible internal difference.
17113                        Token::Ident(s) | Token::QuotedIdent(s)
17114                            if s.eq_ignore_ascii_case("virtual") =>
17115                        {
17116                            self.advance();
17117                            false
17118                        }
17119                        other => {
17120                            return Err(self.err(alloc::format!(
17121                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17122                                 got {other:?}"
17123                            )));
17124                        }
17125                    };
17126                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
17127                    generated_stored_expr = Some(Box::new(expr));
17128                    continue;
17129                }
17130                self.expect_keyword_ident("identity")?;
17131                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17132                // consume the balanced parens and discard (SPG's
17133                // auto-increment is max+1-scan based).
17134                if matches!(self.peek(), Token::LParen) {
17135                    let mut depth = 0usize;
17136                    loop {
17137                        match self.advance() {
17138                            Token::LParen => depth += 1,
17139                            Token::RParen => {
17140                                depth -= 1;
17141                                if depth == 0 {
17142                                    break;
17143                                }
17144                            }
17145                            Token::Eof => {
17146                                return Err(self.err(
17147                                    "unterminated sequence-options parens after IDENTITY".into(),
17148                                ));
17149                            }
17150                            _ => {}
17151                        }
17152                    }
17153                }
17154                auto_increment = true;
17155                // v7.38 (read01) — remember the ALWAYS flavour so the engine
17156                // can reject explicit non-DEFAULT INSERT values (unless
17157                // OVERRIDING SYSTEM VALUE) the way PG does.
17158                identity_always = saw_generated_always;
17159                // PG identity columns are implicitly NOT NULL.
17160                nullable = false;
17161                continue;
17162            }
17163            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17164            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17165            // is accepted today. The "ON" token is an Ident
17166            // (not reserved) — peek before consuming.
17167            if matches!(self.peek(), Token::On)
17168                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17169            {
17170                self.advance(); // ON
17171                self.advance(); // update
17172                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17173                let next = self.peek().clone();
17174                match next {
17175                    Token::Ident(s) | Token::QuotedIdent(s)
17176                        if s.eq_ignore_ascii_case("current_timestamp") =>
17177                    {
17178                        self.advance();
17179                        // Optional `(N)` precision.
17180                        if matches!(self.peek(), Token::LParen) {
17181                            self.advance();
17182                            if !matches!(self.peek(), Token::Integer(_)) {
17183                                return Err(self.err(alloc::format!(
17184                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17185                                    self.peek()
17186                                )));
17187                            }
17188                            self.advance();
17189                            if !matches!(self.peek(), Token::RParen) {
17190                                return Err(self.err(alloc::format!(
17191                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17192                                    self.peek()
17193                                )));
17194                            }
17195                            self.advance();
17196                        }
17197                        on_update_runtime = Some(Expr::FunctionCall {
17198                            name: "now".into(),
17199                            args: Vec::new(),
17200                        });
17201                        continue;
17202                    }
17203                    other => {
17204                        return Err(self.err(alloc::format!(
17205                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17206                        )));
17207                    }
17208                }
17209            }
17210            if matches!(self.peek(), Token::Default) {
17211                if default.is_some() {
17212                    return Err(self.err("DEFAULT specified twice".into()));
17213                }
17214                self.advance();
17215                default = Some(self.parse_expr(0)?);
17216                continue;
17217            }
17218            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17219            // token with NOT NULL and sits EARLIER in the loop than the
17220            // deferrability arm, so without the lookahead it was reported as
17221            // "NOT NULL specified twice" (or "expected NULL after NOT").
17222            if matches!(self.peek(), Token::Not)
17223                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17224            {
17225                // NOT DEFERRABLE — explicit immediate; nothing to carry.
17226                self.consume_optional_deferrable_clauses()?;
17227                continue;
17228            }
17229            if matches!(self.peek(), Token::Not) {
17230                if nullability_seen {
17231                    return Err(self.err("NOT NULL specified twice".into()));
17232                }
17233                self.advance();
17234                if !matches!(self.peek(), Token::Null) {
17235                    return Err(self.err(format!(
17236                        "expected NULL after NOT in column def, got {:?}",
17237                        self.peek()
17238                    )));
17239                }
17240                self.advance();
17241                nullable = false;
17242                nullability_seen = true;
17243                continue;
17244            }
17245            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17246            // "this column is nullable" marker (the default in
17247            // standard SQL anyway). mysqldump emits it routinely
17248            // (`col TYPE NULL DEFAULT NULL` for nullable
17249            // timestamps etc). Accept + no-op.
17250            if matches!(self.peek(), Token::Null) {
17251                if nullability_seen && !nullable {
17252                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
17253                    // sentence, PG18-measured (the table name is the
17254                    // caller's; the column half is exact).
17255                    return Err(self.err(alloc::format!(
17256                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
17257                    )));
17258                }
17259                self.advance();
17260                nullable = true;
17261                nullability_seen = true;
17262                continue;
17263            }
17264            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17265            // arrives as a bare Ident. Match either, case-insensitive.
17266            if let Token::Ident(s) = self.peek()
17267                && (s.eq_ignore_ascii_case("auto_increment")
17268                    || s.eq_ignore_ascii_case("autoincrement"))
17269            {
17270                if auto_increment {
17271                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
17272                }
17273                self.advance();
17274                auto_increment = true;
17275                continue;
17276            }
17277            // v7.9.13 — inline `PRIMARY KEY` column constraint
17278            // (mailrs F1). Implies `NOT NULL`. The engine creates
17279            // a BTree index for the PK column at CREATE TABLE time
17280            // so FK parent-side index lookups resolve.
17281            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17282            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17283            // spelling was a parse error, so a pg_dump carrying one stopped
17284            // mid-restore. The clauses are consumed by the same helper the FK
17285            // path has used since round 288 and recorded nowhere: SPG enforces
17286            // the constraint IMMEDIATELY either way, which fails earlier than
17287            // PG inside a transaction that violates-then-repairs — a refusal,
17288            // not a wrong answer. True deferral is the open remainder of F08.
17289            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17290                || (matches!(self.peek(), Token::Not)
17291                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17292            {
17293                // v7.39 (round 711) — CARRIED now (the storing half of
17294                // F08); round 621 only consumed.
17295                let (d, idef) = self.consume_deferrable_clauses_timed()?;
17296                constraint_deferrable |= d;
17297                constraint_initially_deferred |= idef;
17298                continue;
17299            }
17300            if let Token::Ident(s) = self.peek()
17301                && s.eq_ignore_ascii_case("primary")
17302            {
17303                if is_primary_key {
17304                    return Err(self.err("PRIMARY KEY specified twice".into()));
17305                }
17306                // Peek-ahead for the required `KEY` token.
17307                let next = self.tokens.get(self.pos + 1);
17308                let next_is_key = matches!(
17309                    next,
17310                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17311                );
17312                if !next_is_key {
17313                    return Err(self.err(format!(
17314                        "expected KEY after PRIMARY in column def, got {:?}",
17315                        next
17316                    )));
17317                }
17318                self.advance(); // PRIMARY
17319                self.advance(); // KEY
17320                is_primary_key = true;
17321                if nullability_seen && nullable {
17322                    return Err(self.err(
17323                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17324                    ));
17325                }
17326                nullable = false;
17327                nullability_seen = true;
17328                continue;
17329            }
17330            // v7.13.0 — inline `UNIQUE` column constraint
17331            // (mailrs round-5 G2). Fold into a single-column
17332            // table-level UNIQUE at CREATE TABLE post-process time.
17333            if let Token::Ident(s) = self.peek()
17334                && s.eq_ignore_ascii_case("unique")
17335            {
17336                if is_unique {
17337                    return Err(self.err("UNIQUE specified twice".into()));
17338                }
17339                self.advance();
17340                is_unique = true;
17341                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17342                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17343                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17344                    let n1 = self.tokens.get(self.pos + 1);
17345                    let n2 = self.tokens.get(self.pos + 2);
17346                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17347                        self.advance(); // NULLS
17348                        self.advance(); // NOT
17349                        self.advance(); // DISTINCT
17350                        unique_nulls_not_distinct = true;
17351                    } else if matches!(n1, Some(Token::Distinct)) {
17352                        self.advance(); // NULLS
17353                        self.advance(); // DISTINCT
17354                    }
17355                }
17356                continue;
17357            }
17358            // v7.13.0 — inline `CHECK (<expr>)` column constraint
17359            // (mailrs round-5 G3). PG semantics: column-level
17360            // CHECK is equivalent to a table-level CHECK. Multiple
17361            // inline CHECKs on the same column AND together.
17362            if let Token::Ident(s) = self.peek()
17363                && s.eq_ignore_ascii_case("check")
17364            {
17365                self.advance();
17366                if !matches!(self.peek(), Token::LParen) {
17367                    return Err(self.err(alloc::format!(
17368                        "expected '(' after CHECK in column def, got {:?}",
17369                        self.peek()
17370                    )));
17371                }
17372                self.advance();
17373                let pred = self.parse_expr(0)?;
17374                if !matches!(self.peek(), Token::RParen) {
17375                    return Err(self.err(alloc::format!(
17376                        "expected ')' to close CHECK predicate, got {:?}",
17377                        self.peek()
17378                    )));
17379                }
17380                self.advance();
17381                check = Some(match check.take() {
17382                    Some(prev) => Expr::Binary {
17383                        op: BinOp::And,
17384                        lhs: Box::new(prev),
17385                        rhs: Box::new(pred),
17386                    },
17387                    None => pred,
17388                });
17389                continue;
17390            }
17391            break;
17392        }
17393        Ok(ColumnDef {
17394            name,
17395            ty,
17396            nullable,
17397            default,
17398            auto_increment,
17399            is_primary_key,
17400            is_unique,
17401            unique_nulls_not_distinct,
17402            constraint_deferrable,
17403            constraint_initially_deferred,
17404            check,
17405            user_type_ref,
17406            on_update_runtime,
17407            collation,
17408            collation_explicit,
17409            collation_name,
17410            is_unsigned,
17411            inline_enum_variants,
17412            inline_set_variants,
17413            generated_stored_expr,
17414            identity_always,
17415            mysql_int_width,
17416            mysql_fsp,
17417        })
17418    }
17419
17420    /// `NUMERIC` may appear without parameters, with one (precision
17421    /// only, scale=0), or with both. Returns `(precision, scale)` with
17422    /// 0 = unspecified for the bare form.
17423    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17424        if !matches!(self.peek(), Token::LParen) {
17425            // Bare `NUMERIC` — PG treats this as "unlimited precision";
17426            // we surface it as precision=0 to mean "unconstrained" so
17427            // the engine doesn't need a separate variant.
17428            return Ok((0, 0));
17429        }
17430        self.advance();
17431        // v7.39 (round 272) — PG's declared precision runs to 1000, and
17432        // it words the out-of-range case with the value it saw. SPG
17433        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17434        // accepts failed to parse at all; values wider than i128 are
17435        // carried by the arbitrary-precision form.
17436        let precision = match self.advance() {
17437            Token::Integer(n) if (1..=1000).contains(&n) => {
17438                u16::try_from(n).expect("range-checked")
17439            }
17440            Token::Integer(n) => {
17441                return Err(ParseError {
17442                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17443                    token_pos: self.consumed_pos(),
17444                });
17445            }
17446            other => {
17447                return Err(ParseError {
17448                    message: format!(
17449                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17450                    ),
17451                    token_pos: self.consumed_pos(),
17452                });
17453            }
17454        };
17455        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17456        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17457        // then overflows). A negative scale rounds to tens / hundreds / …
17458        let scale = if matches!(self.peek(), Token::Comma) {
17459            self.advance();
17460            let neg = if matches!(self.peek(), Token::Minus) {
17461                self.advance();
17462                true
17463            } else {
17464                false
17465            };
17466            match self.advance() {
17467                Token::Integer(n) => {
17468                    let signed = if neg { -n } else { n };
17469                    if !(-1000..=1000).contains(&signed) {
17470                        return Err(ParseError {
17471                            message: format!(
17472                                "NUMERIC scale {signed} must be between -1000 and 1000"
17473                            ),
17474                            token_pos: self.consumed_pos(),
17475                        });
17476                    }
17477                    i16::try_from(signed).expect("range-checked")
17478                }
17479                other => {
17480                    return Err(ParseError {
17481                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
17482                        token_pos: self.consumed_pos(),
17483                    });
17484                }
17485            }
17486        } else {
17487            0
17488        };
17489        if !matches!(self.peek(), Token::RParen) {
17490            return Err(self.err(format!(
17491                "expected ')' to close NUMERIC params, got {:?}",
17492                self.peek()
17493            )));
17494        }
17495        self.advance();
17496        Ok((precision, scale))
17497    }
17498
17499    /// Parse `(N)` where `N` is a positive integer literal — used by the
17500    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17501    /// for the error message.
17502    /// v6.0.1: parse the optional `USING <encoding>` clause that
17503    /// follows `VECTOR(N)` in a column definition. Missing clause
17504    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17505    /// ident → `ParseError` listing the encodings recognised today.
17506    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17507        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17508            return Ok(VecEncoding::F32);
17509        }
17510        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17511        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17512        // consume the token when the very next token is a known
17513        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17514        // `USING` for the caller — it's the rewrite-expression form.
17515        let n1 = self.tokens.get(self.pos + 1);
17516        let next_is_encoding = matches!(
17517            n1,
17518            Some(Token::Ident(s))
17519                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17520        );
17521        if !next_is_encoding {
17522            return Ok(VecEncoding::F32);
17523        }
17524        self.advance();
17525        let enc_ident = match self.advance() {
17526            Token::Ident(s) => s,
17527            other => {
17528                return Err(self.err(format!(
17529                    "expected vector encoding after USING, got {other:?}"
17530                )));
17531            }
17532        };
17533        match enc_ident.to_ascii_lowercase().as_str() {
17534            "sq8" => Ok(VecEncoding::Sq8),
17535            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17536            // binary16 per-element storage.
17537            "half" => Ok(VecEncoding::F16),
17538            other => Err(self.err(format!(
17539                "unknown vector encoding {other:?}; supported: SQ8, HALF"
17540            ))),
17541        }
17542    }
17543
17544    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17545    /// without consuming it. Returns `Some(N)` when the next
17546    /// tokens are `( <int> )`; None otherwise. Used by the
17547    /// TINYINT classifier to decide whether to map to Bool or
17548    /// SmallInt.
17549    fn peek_optional_paren_size_value(&self) -> Option<i64> {
17550        if !matches!(self.peek(), Token::LParen) {
17551            return None;
17552        }
17553        let next = self.tokens.get(self.pos + 1)?;
17554        let n = match next {
17555            Token::Integer(n) => *n,
17556            _ => return None,
17557        };
17558        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17559            return None;
17560        }
17561        Some(n)
17562    }
17563
17564    /// v7.14.0 — consume an optional MySQL display-width
17565    /// parenthesised number after an integer type, returning
17566    /// nothing. `TINYINT(1)` etc.
17567    /// v7.39 (round 360) — does the parenthesised group ahead contain a
17568    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17569    fn peek_paren_has_comma(&self) -> bool {
17570        let mut i = self.pos + 1;
17571        let mut depth = 1usize;
17572        while depth > 0 {
17573            match self.tokens.get(i) {
17574                Some(Token::LParen) => depth += 1,
17575                Some(Token::RParen) => depth -= 1,
17576                Some(Token::Comma) if depth == 1 => return true,
17577                None | Some(Token::Eof) => return false,
17578                _ => {}
17579            }
17580            i += 1;
17581        }
17582        false
17583    }
17584
17585    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17586    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17587    /// fractional-seconds precision that drives write truncation and render
17588    /// padding, where `consume_optional_paren_size` throws it away.
17589    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17590    fn take_optional_paren_size(&mut self) -> Option<u8> {
17591        let Some(Token::Integer(n)) = self
17592            .tokens
17593            .get(self.pos + 1)
17594            .filter(|_| matches!(self.peek(), Token::LParen))
17595            .cloned()
17596        else {
17597            self.consume_optional_paren_size();
17598            return None;
17599        };
17600        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17601            self.consume_optional_paren_size();
17602            return None;
17603        }
17604        self.consume_optional_paren_size();
17605        u8::try_from(n).ok()
17606    }
17607
17608    fn consume_optional_paren_size(&mut self) {
17609        if !matches!(self.peek(), Token::LParen) {
17610            return;
17611        }
17612        self.advance();
17613        // Skip until matching RParen (allow nested or any tokens).
17614        let mut depth = 1usize;
17615        while depth > 0 {
17616            match self.peek() {
17617                Token::LParen => depth += 1,
17618                Token::RParen => depth -= 1,
17619                Token::Eof => return,
17620                _ => {}
17621            }
17622            self.advance();
17623        }
17624    }
17625
17626    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17627        if !matches!(self.peek(), Token::LParen) {
17628            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17629        }
17630        self.advance();
17631        let n = match self.advance() {
17632            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17633                message: format!("{label} size too large: {n}"),
17634                token_pos: self.consumed_pos(),
17635            })?,
17636            other => {
17637                return Err(ParseError {
17638                    message: format!("expected positive integer {label} size, got {other:?}"),
17639                    token_pos: self.consumed_pos(),
17640                });
17641            }
17642        };
17643        if !matches!(self.peek(), Token::RParen) {
17644            return Err(self.err(format!(
17645                "expected ')' after {label} size, got {:?}",
17646                self.peek()
17647            )));
17648        }
17649        self.advance();
17650        Ok(n)
17651    }
17652
17653    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17654    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17655    /// key, like MySQL) whose action skips conflicting rows.
17656    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17657    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17658    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17659    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17660    /// common bulk-upsert spellings —
17661    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17662    ///     REPLACE INTO t SELECT …
17663    /// — were a parse error / a duplicate-key failure respectively.
17664    ///
17665    /// Precedence: an explicitly written clause beats a statement-level flag.
17666    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17667    /// implicit `REPLACE` and `IGNORE` lowerings.
17668    fn parse_insert_conflict_clause(
17669        &mut self,
17670        replace: bool,
17671        ignore: bool,
17672    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17673        if let Some(c) = self.parse_optional_on_duplicate_key()? {
17674            return Ok(Some(c));
17675        }
17676        if let Some(c) = self.parse_optional_on_conflict()? {
17677            return Ok(Some(c));
17678        }
17679        if replace {
17680            // REPLACE INTO = delete-then-insert, which PG spells as
17681            // `ON CONFLICT DO UPDATE SET` over every column; the engine
17682            // reads an empty assignment list as "take the incoming row".
17683            return Ok(Some(crate::ast::OnConflictClause {
17684                target_columns: Vec::new(),
17685                index_where: None,
17686                constraint_name: None,
17687                mysql_lowered: true,
17688                action: crate::ast::OnConflictAction::Update {
17689                    assignments: Vec::new(),
17690                    where_: None,
17691                },
17692            }));
17693        }
17694        if ignore {
17695            return Ok(Some(Self::insert_ignore_clause()));
17696        }
17697        Ok(None)
17698    }
17699
17700    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17701    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17702    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17703    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17704    fn parse_optional_on_duplicate_key(
17705        &mut self,
17706    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17707        if !(matches!(self.peek(), Token::On)
17708            && matches!(self.tokens.get(self.pos + 1),
17709                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17710        {
17711            return Ok(None);
17712        }
17713        self.advance(); // ON
17714        self.advance(); // DUPLICATE
17715        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17716            return Err(self.err(format!(
17717                "expected KEY after ON DUPLICATE, got {:?}",
17718                self.peek()
17719            )));
17720        }
17721        self.advance();
17722        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17723            return Err(self.err(format!(
17724                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17725                self.peek()
17726            )));
17727        }
17728        self.advance();
17729        let mut assignments: Vec<(String, Expr)> = Vec::new();
17730        loop {
17731            let col = self.expect_ident_like()?;
17732            if !matches!(self.peek(), Token::Eq) {
17733                return Err(self.err(format!(
17734                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17735                    self.peek()
17736                )));
17737            }
17738            self.advance();
17739            let mut expr = self.parse_expr(0)?;
17740            Self::rewrite_mysql_values_refs(&mut expr);
17741            assignments.push((col, expr));
17742            if matches!(self.peek(), Token::Comma) {
17743                self.advance();
17744                continue;
17745            }
17746            break;
17747        }
17748        Ok(Some(crate::ast::OnConflictClause {
17749            target_columns: Vec::new(),
17750            index_where: None,
17751            constraint_name: None,
17752            mysql_lowered: true,
17753            action: crate::ast::OnConflictAction::Update {
17754                assignments,
17755                where_: None,
17756            },
17757        }))
17758    }
17759
17760    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17761        crate::ast::OnConflictClause {
17762            target_columns: Vec::new(),
17763            index_where: None,
17764            constraint_name: None,
17765            mysql_lowered: true,
17766            action: crate::ast::OnConflictAction::Nothing,
17767        }
17768    }
17769
17770    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17771        debug_assert!(
17772            matches!(self.peek(), Token::Insert)
17773                || (replace
17774                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17775        );
17776        self.advance();
17777        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17778        // would raise a duplicate-key error instead of failing the statement,
17779        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17780        // plain ident to the lexer; only the MySQL dialect accepts it here.
17781        let ignore = self.mysql_dialect
17782            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17783        if ignore {
17784            self.advance();
17785        }
17786        if !matches!(self.peek(), Token::Into) {
17787            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17788        }
17789        self.advance();
17790        let table = self.expect_ident_like()?;
17791        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17792        // grammar requires the AS keyword here (a bare identifier would be
17793        // ambiguous with a column list). The alias is what the ON CONFLICT
17794        // DO UPDATE expressions refer to the target row by.
17795        let alias = if matches!(self.peek(), Token::As) {
17796            self.advance();
17797            Some(self.expect_ident_like()?)
17798        } else {
17799            None
17800        };
17801        // v7.39 (round 428) — MySQL's SET-form INSERT:
17802        //     INSERT INTO t SET a = 1, b = 'x'
17803        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
17804        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
17805        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
17806        // measured). So it lowers to the column list + one VALUES row and
17807        // rejoins the ordinary path, which already handles every one of
17808        // those. PG has no such spelling, hence the dialect gate.
17809        if self.mysql_dialect
17810            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
17811        {
17812            self.advance(); // SET
17813            let mut names = Vec::new();
17814            let mut values = Vec::new();
17815            loop {
17816                names.push(self.expect_ident_like()?);
17817                if !matches!(self.peek(), Token::Eq) {
17818                    return Err(self.err(alloc::format!(
17819                        "expected '=' in INSERT … SET, got {:?}",
17820                        self.peek()
17821                    )));
17822                }
17823                self.advance();
17824                // `SET a = DEFAULT` rides the same `__column_default` marker
17825                // the VALUES-row and UPDATE-SET paths use; the INSERT
17826                // executor resolves it against the target column.
17827                if matches!(self.peek(), Token::Default) {
17828                    self.advance();
17829                    values.push(Expr::FunctionCall {
17830                        name: "__column_default".to_string(),
17831                        args: Vec::new(),
17832                    });
17833                } else {
17834                    values.push(self.parse_expr(0)?);
17835                }
17836                if matches!(self.peek(), Token::Comma) {
17837                    self.advance();
17838                    continue;
17839                }
17840                break;
17841            }
17842            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17843            let returning = self.parse_optional_returning()?;
17844            return Ok(Statement::Insert(InsertStatement {
17845                ctes: Vec::new(),
17846                table,
17847                alias,
17848                columns: Some(names),
17849                rows: alloc::vec![values],
17850                select_source: None,
17851                // MySQL's SET form has no `OVERRIDING …` clause (that is
17852                // PG's identity-column spelling).
17853                overriding: Overriding::None,
17854                mysql_ignore: ignore,
17855                on_conflict,
17856                returning,
17857            }));
17858        }
17859        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
17860        // v7.39 (round 151) — a SELECT or WITH right after the paren is
17861        // a parenthesized query source instead (PG select_with_parens:
17862        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
17863        // both keywords are reserved in PG, so no column list can start
17864        // with them.
17865        let columns = if matches!(self.peek(), Token::LParen) {
17866            self.advance();
17867            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17868                let select_stmt = if self.peek_is_with_kw() {
17869                    self.advance();
17870                    self.parse_nested_with_select()?
17871                } else {
17872                    match self.parse_select_stmt()? {
17873                        Statement::Select(s) => s,
17874                        other => {
17875                            return Err(self.err(alloc::format!(
17876                                "expected SELECT in parenthesized INSERT source, got {other:?}"
17877                            )));
17878                        }
17879                    }
17880                };
17881                if !matches!(self.peek(), Token::RParen) {
17882                    return Err(self.err(format!(
17883                        "expected ')' after parenthesized INSERT source, got {:?}",
17884                        self.peek()
17885                    )));
17886                }
17887                self.advance();
17888                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17889                let returning = self.parse_optional_returning()?;
17890                return Ok(Statement::Insert(InsertStatement {
17891                    ctes: Vec::new(),
17892                    table,
17893                    alias: alias.clone(),
17894                    columns: None,
17895                    rows: Vec::new(),
17896                    select_source: Some(Box::new(select_stmt)),
17897                    on_conflict,
17898                    returning,
17899                    overriding: Overriding::None,
17900                    mysql_ignore: ignore,
17901                }));
17902            }
17903            let mut names = Vec::new();
17904            loop {
17905                names.push(self.expect_ident_like()?);
17906                match self.peek() {
17907                    Token::Comma => {
17908                        self.advance();
17909                    }
17910                    Token::RParen => {
17911                        self.advance();
17912                        break;
17913                    }
17914                    other => {
17915                        return Err(self.err(format!(
17916                            "expected ',' or ')' in INSERT column list, got {other:?}"
17917                        )));
17918                    }
17919                }
17920            }
17921            Some(names)
17922        } else {
17923            None
17924        };
17925        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
17926        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
17927        // is captured on the statement so the engine can apply PG's
17928        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
17929        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
17930        {
17931            self.advance();
17932            let which = self.expect_ident_like()?;
17933            let ov = if which.eq_ignore_ascii_case("system") {
17934                Overriding::System
17935            } else if which.eq_ignore_ascii_case("user") {
17936                Overriding::User
17937            } else {
17938                return Err(self.err(format!(
17939                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
17940                )));
17941            };
17942            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
17943                return Err(self.err(format!(
17944                    "expected VALUE after OVERRIDING {}, got {:?}",
17945                    which.to_ascii_uppercase(),
17946                    self.peek()
17947                )));
17948            }
17949            self.advance();
17950            ov
17951        } else {
17952            Overriding::None
17953        };
17954        // `INSERT INTO t DEFAULT VALUES` — a single row made
17955        // entirely of column defaults. Lower to the permuted
17956        // column-list path with an empty list: every schema column
17957        // is unmapped, so the engine fills each from its default
17958        // (serials advance, plain defaults evaluate, the rest NULL).
17959        if matches!(self.peek(), Token::Default) {
17960            self.advance();
17961            if !matches!(self.peek(), Token::Values) {
17962                return Err(self.err(format!(
17963                    "expected VALUES after DEFAULT in INSERT, got {:?}",
17964                    self.peek()
17965                )));
17966            }
17967            self.advance();
17968            if columns.is_some() {
17969                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
17970            }
17971            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17972            let returning = self.parse_optional_returning()?;
17973            return Ok(Statement::Insert(InsertStatement {
17974                ctes: Vec::new(),
17975                table,
17976                alias: alias.clone(),
17977                columns: Some(Vec::new()),
17978                rows: alloc::vec![Vec::new()],
17979                select_source: None,
17980                on_conflict,
17981                returning,
17982                overriding,
17983                mysql_ignore: ignore,
17984            }));
17985        }
17986        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
17987        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
17988        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
17989        // SELECT …`) heads the SOURCE select, as in PG (the statement's
17990        // own WITH comes before INSERT).
17991        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17992            let select_stmt = if self.peek_is_with_kw() {
17993                self.advance();
17994                self.parse_nested_with_select()?
17995            } else {
17996                match self.parse_select_stmt()? {
17997                    Statement::Select(s) => s,
17998                    other => {
17999                        return Err(self.err(alloc::format!(
18000                            "expected SELECT after INSERT INTO ... target, got {other:?}"
18001                        )));
18002                    }
18003                }
18004            };
18005            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18006            let returning = self.parse_optional_returning()?;
18007            return Ok(Statement::Insert(InsertStatement {
18008                ctes: Vec::new(),
18009                table,
18010                alias: alias.clone(),
18011                columns,
18012                rows: Vec::new(),
18013                select_source: Some(Box::new(select_stmt)),
18014                on_conflict,
18015                returning,
18016                overriding,
18017                mysql_ignore: ignore,
18018            }));
18019        }
18020        if !matches!(self.peek(), Token::Values) {
18021            return Err(self.err(format!(
18022                "expected VALUES or SELECT after table name, got {:?}",
18023                self.peek()
18024            )));
18025        }
18026        self.advance();
18027        if !matches!(self.peek(), Token::LParen) {
18028            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18029        }
18030        let mut rows = Vec::new();
18031        loop {
18032            // Each iteration consumes one `(expr, expr, …)` tuple.
18033            if !matches!(self.peek(), Token::LParen) {
18034                return Err(self.err(format!(
18035                    "expected '(' for next VALUES tuple, got {:?}",
18036                    self.peek()
18037                )));
18038            }
18039            self.advance();
18040            let mut tuple = Vec::new();
18041            loop {
18042                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18043                // the column's declared default for that slot. Rides out as the
18044                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18045                // path uses; the INSERT executor resolves it per target column.
18046                if matches!(self.peek(), Token::Default) {
18047                    self.advance();
18048                    tuple.push(Expr::FunctionCall {
18049                        name: "__column_default".to_string(),
18050                        args: Vec::new(),
18051                    });
18052                } else {
18053                    tuple.push(self.parse_expr(0)?);
18054                }
18055                match self.peek() {
18056                    Token::Comma => {
18057                        self.advance();
18058                    }
18059                    Token::RParen => {
18060                        self.advance();
18061                        break;
18062                    }
18063                    other => {
18064                        return Err(self.err(format!(
18065                            "expected ',' or ')' in VALUES tuple, got {other:?}"
18066                        )));
18067                    }
18068                }
18069            }
18070            if tuple.is_empty() {
18071                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18072            }
18073            rows.push(tuple);
18074            // Continue with comma-separated tuples.
18075            if matches!(self.peek(), Token::Comma) {
18076                self.advance();
18077            } else {
18078                break;
18079            }
18080        }
18081        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18082        // to ON CONFLICT DO UPDATE with an empty conflict target
18083        // (the engine picks the table's first unique index, which
18084        // matches MySQL's any-unique-key behaviour for the common
18085        // single-key case). `VALUES(col)` in the assignments is
18086        // MySQL's spelling of EXCLUDED.col.
18087        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18088        let returning = self.parse_optional_returning()?;
18089        Ok(Statement::Insert(InsertStatement {
18090            ctes: Vec::new(),
18091            table,
18092            alias,
18093            columns,
18094            rows,
18095            select_source: None,
18096            on_conflict,
18097            returning,
18098            overriding,
18099            mysql_ignore: ignore,
18100        }))
18101    }
18102
18103    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18104    /// the incoming row's value — exactly PG's EXCLUDED.col.
18105    fn rewrite_mysql_values_refs(e: &mut Expr) {
18106        match e {
18107            Expr::FunctionCall { name, args }
18108                if name.eq_ignore_ascii_case("values")
18109                    && args.len() == 1
18110                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18111            {
18112                let Expr::Column(c) = &args[0] else {
18113                    unreachable!("guarded above");
18114                };
18115                *e = Expr::Column(crate::ast::ColumnName {
18116                    qualifier: Some("EXCLUDED".to_string()),
18117                    name: c.name.clone(),
18118                });
18119            }
18120            Expr::FunctionCall { args, .. } => {
18121                for a in args {
18122                    Self::rewrite_mysql_values_refs(a);
18123                }
18124            }
18125            Expr::Binary { lhs, rhs, .. } => {
18126                Self::rewrite_mysql_values_refs(lhs);
18127                Self::rewrite_mysql_values_refs(rhs);
18128            }
18129            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18130                Self::rewrite_mysql_values_refs(expr);
18131            }
18132            Expr::Case {
18133                operand,
18134                branches,
18135                else_branch,
18136            } => {
18137                if let Some(op) = operand {
18138                    Self::rewrite_mysql_values_refs(op);
18139                }
18140                for (w, t) in branches {
18141                    Self::rewrite_mysql_values_refs(w);
18142                    Self::rewrite_mysql_values_refs(t);
18143                }
18144                if let Some(el) = else_branch {
18145                    Self::rewrite_mysql_values_refs(el);
18146                }
18147            }
18148            _ => {}
18149        }
18150    }
18151
18152    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18153    /// clause sitting between the INSERT body and the trailing
18154    /// RETURNING. All keywords come in as bare idents; `ON` is
18155    /// a reserved Token though.
18156    fn parse_optional_on_conflict(
18157        &mut self,
18158    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18159        if !matches!(self.peek(), Token::On) {
18160            return Ok(None);
18161        }
18162        // Peek further: we want exactly "ON CONFLICT ...". If the
18163        // next ident isn't "conflict", let some other parser handle.
18164        let next_is_conflict = matches!(
18165            self.tokens.get(self.pos + 1),
18166            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18167        );
18168        if !next_is_conflict {
18169            return Ok(None);
18170        }
18171        self.advance(); // ON
18172        self.advance(); // CONFLICT
18173        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18174        // the constraint instead of listing columns (the pg_dump
18175        // form); the engine resolves it.
18176        let mut constraint_name: Option<String> = None;
18177        if matches!(self.peek(), Token::On) {
18178            self.advance(); // ON
18179            match self.advance() {
18180                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18181                }
18182                other => {
18183                    return Err(self.err(alloc::format!(
18184                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18185                    )));
18186                }
18187            }
18188            constraint_name = Some(self.expect_ident_like()?);
18189        }
18190        // Optional `(col [, col]*)` target list.
18191        let mut target_columns: Vec<String> = Vec::new();
18192        if matches!(self.peek(), Token::LParen) {
18193            self.advance();
18194            loop {
18195                target_columns.push(self.expect_ident_like()?);
18196                match self.peek() {
18197                    Token::Comma => {
18198                        self.advance();
18199                    }
18200                    Token::RParen => {
18201                        self.advance();
18202                        break;
18203                    }
18204                    other => {
18205                        return Err(self.err(alloc::format!(
18206                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18207                        )));
18208                    }
18209                }
18210            }
18211        }
18212        // v7.39 (round 240) — optional index predicate after the target
18213        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18214        // PARTIAL unique index; SPG's arbiters are full indexes, which
18215        // satisfy any predicate, so it is parsed and carried but not
18216        // consulted (recorded residual: partial-unique-index arbiters).
18217        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18218            self.advance();
18219            Some(self.parse_expr(0)?)
18220        } else {
18221            None
18222        };
18223        // Required `DO`.
18224        match self.advance() {
18225            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18226            other => {
18227                return Err(self.err(alloc::format!(
18228                    "expected DO after ON CONFLICT [(…)], got {other:?}"
18229                )));
18230            }
18231        }
18232        // Action: NOTHING | UPDATE SET …
18233        let action = match self.advance() {
18234            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18235                crate::ast::OnConflictAction::Nothing
18236            }
18237            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18238                self.parse_on_conflict_update_action()?
18239            }
18240            other => {
18241                return Err(self.err(alloc::format!(
18242                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18243                )));
18244            }
18245        };
18246        Ok(Some(crate::ast::OnConflictClause {
18247            target_columns,
18248            index_where,
18249            constraint_name,
18250            mysql_lowered: false,
18251            action,
18252        }))
18253    }
18254
18255    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18256    /// `SET col = expr [, …] [WHERE cond]`. Caller already
18257    /// consumed `UPDATE`.
18258    fn parse_on_conflict_update_action(
18259        &mut self,
18260    ) -> Result<crate::ast::OnConflictAction, ParseError> {
18261        // `SET`
18262        match self.advance() {
18263            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18264            other => {
18265                return Err(self.err(alloc::format!(
18266                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18267                )));
18268            }
18269        }
18270        let mut assignments: Vec<(String, Expr)> = Vec::new();
18271        loop {
18272            let col = self.expect_ident_like()?;
18273            if !matches!(self.peek(), Token::Eq) {
18274                return Err(self.err(alloc::format!(
18275                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18276                    self.peek()
18277                )));
18278            }
18279            self.advance();
18280            let value = self.parse_expr(0)?;
18281            assignments.push((col, value));
18282            if matches!(self.peek(), Token::Comma) {
18283                self.advance();
18284                continue;
18285            }
18286            break;
18287        }
18288        let where_ = if matches!(self.peek(), Token::Where) {
18289            self.advance();
18290            Some(self.parse_expr(0)?)
18291        } else {
18292            None
18293        };
18294        Ok(crate::ast::OnConflictAction::Update {
18295            assignments,
18296            where_,
18297        })
18298    }
18299
18300    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18301        let mut items = Vec::new();
18302        // v7.39 (round 341, V66) — PG's target list may be EMPTY
18303        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18304        // answers one zero-column row per row of t, and a bare `SELECT`
18305        // answers a single zero-column row. SPG required at least one
18306        // item, so both were syntax errors. Recognised by the token that
18307        // follows — nothing that can start an expression appears here.
18308        if self.select_list_is_empty_here() {
18309            return Ok(items);
18310        }
18311        loop {
18312            items.push(self.parse_select_item()?);
18313            if matches!(self.peek(), Token::Comma) {
18314                self.advance();
18315            } else {
18316                break;
18317            }
18318        }
18319        Ok(items)
18320    }
18321
18322    /// Is the target list empty at this point — i.e. does the next token
18323    /// end the SELECT's item list rather than start an item?
18324    fn select_list_is_empty_here(&self) -> bool {
18325        match self.peek() {
18326            Token::From
18327            | Token::Where
18328            | Token::Group
18329            | Token::Having
18330            | Token::Order
18331            | Token::Limit
18332            | Token::Offset
18333            | Token::Semicolon
18334            | Token::RParen
18335            | Token::Union
18336            | Token::Except
18337            | Token::Eof => true,
18338            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18339            // with unreserved keywords, so they arrive as plain idents.
18340            Token::Ident(s) => {
18341                s.eq_ignore_ascii_case("fetch")
18342                    || s.eq_ignore_ascii_case("window")
18343                    || s.eq_ignore_ascii_case("intersect")
18344            }
18345            _ => false,
18346        }
18347    }
18348
18349    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18350        if matches!(self.peek(), Token::Star) {
18351            self.advance();
18352            return Ok(SelectItem::Wildcard);
18353        }
18354        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18355        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18356        // choke on the `*` ("expected identifier, got Star"). The lookahead is
18357        // `<ident> . *` with nothing binding tighter.
18358        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18359            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18360                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18361            {
18362                self.advance(); // qualifier
18363                self.advance(); // .
18364                self.advance(); // *
18365                return Ok(SelectItem::QualifiedWildcard(q));
18366            }
18367        }
18368        let start_tok = self.pos;
18369        let expr = self.parse_expr(0)?;
18370        let end_tok = self.consumed_pos();
18371        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18372        // multi-column function returns into columns. Marked here and lowered in
18373        // `parse_bare_select`, where the FROM clause is in hand.
18374        if matches!(self.peek(), Token::Dot)
18375            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18376        {
18377            self.advance(); // .
18378            self.advance(); // *
18379            return Ok(SelectItem::Expr {
18380                expr: Expr::FunctionCall {
18381                    name: "__record_expand".to_string(),
18382                    args: alloc::vec![expr],
18383                },
18384                alias: None,
18385            });
18386        }
18387        let alias = match self.parse_optional_alias()? {
18388            Some(a) => Some(a),
18389            None => self.mysql_item_label(&expr, start_tok, end_tok),
18390        };
18391        Ok(SelectItem::Expr { expr, alias })
18392    }
18393
18394    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18395    /// carries no `AS`, filled in here so every downstream path reports it
18396    /// without knowing the rule. `None` leaves the item un-aliased, which is
18397    /// what a PG session always gets.
18398    ///
18399    /// Measured against MariaDB 11, three rules and no more:
18400    ///
18401    /// | item             | label      | why                          |
18402    /// |------------------|------------|------------------------------|
18403    /// | `lbl.a`          | `a`        | a column reports its name    |
18404    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
18405    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
18406    ///
18407    /// The third is why this lives in the parser at all: the label is the
18408    /// text the client WROTE, down to the spacing, so it cannot be printed
18409    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18410    ///
18411    /// Comments survive, and that is right: through a `mariadb` CLI both
18412    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
18413    /// CLIENT stripping the comment before it sends. Asked over the raw
18414    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18415    /// produces.
18416    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18417        if !self.mysql_dialect {
18418            return None;
18419        }
18420        match expr {
18421            // A column already reports its own name downstream; naming it
18422            // again here would only re-state the qualifier the label drops.
18423            Expr::Column(_) => None,
18424            Expr::Literal(Literal::String(v)) => Some(v.clone()),
18425            _ => self.source_span(start_tok, end_tok).map(str::to_string),
18426        }
18427    }
18428
18429    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18430    /// consumed VALUES keyword. Each row lowers to a constant SELECT
18431    /// with PG's default column1..columnN names; subsequent rows
18432    /// chain as UNION ALL peers. Shared by the FROM-position
18433    /// `( VALUES … )` arm and the top-level bare VALUES statement.
18434    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18435        let mut row_selects: Vec<SelectStatement> = Vec::new();
18436        loop {
18437            if !matches!(self.peek(), Token::LParen) {
18438                return Err(self.err(alloc::format!(
18439                    "expected '(' to start a VALUES row, got {:?}",
18440                    self.peek()
18441                )));
18442            }
18443            self.advance(); // (
18444            let mut items: Vec<SelectItem> = Vec::new();
18445            loop {
18446                let expr = self.parse_expr(0)?;
18447                items.push(SelectItem::Expr {
18448                    expr,
18449                    alias: Some(alloc::format!("column{}", items.len() + 1)),
18450                });
18451                match self.peek() {
18452                    Token::Comma => {
18453                        self.advance();
18454                    }
18455                    Token::RParen => break,
18456                    other => {
18457                        return Err(self.err(alloc::format!(
18458                            "expected ',' or ')' in VALUES row, got {other:?}"
18459                        )));
18460                    }
18461                }
18462            }
18463            self.advance(); // )
18464            row_selects.push(SelectStatement {
18465                locking: None,
18466                ctes: Vec::new(),
18467                distinct: false,
18468                distinct_on: Vec::new(),
18469                items,
18470                from: None,
18471                where_: None,
18472                group_by: None,
18473                group_by_all: false,
18474                having: None,
18475                unions: Vec::new(),
18476                order_by: Vec::new(),
18477                limit: None,
18478                offset: None,
18479                limit_with_ties: false,
18480                window_check_exprs: Vec::new(),
18481            });
18482            if matches!(self.peek(), Token::Comma) {
18483                self.advance();
18484                continue;
18485            }
18486            break;
18487        }
18488        let mut head = row_selects.remove(0);
18489        head.unions = row_selects
18490            .into_iter()
18491            .map(|s| (UnionKind::All, s))
18492            .collect();
18493        Ok(head)
18494    }
18495
18496    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18497        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18498        // children. It was read as a table NAMED `only`, so the query
18499        // failed on `relation "only" does not exist`.
18500        //
18501        // v7.39 (round 644) — and it is no longer a no-op. Round 621
18502        // absorbed the keyword, reasoning that SPG's children are
18503        // separate relations a plain scan does not descend into, so ONLY
18504        // already described the scan. That stopped being true when a
18505        // partition parent started unioning its children: measured,
18506        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18507        // where PG answers 0. The flag is carried now.
18508        let mut only = false;
18509        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18510            && matches!(
18511                self.tokens.get(self.pos + 1),
18512                Some(Token::Ident(_) | Token::QuotedIdent(_))
18513            )
18514        {
18515            only = true;
18516            self.advance();
18517        }
18518        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18519        // for these SRFs the keyword is noise at parse time: the
18520        // join executor already substitutes outer-column references
18521        // into unnest_expr / generate_series_args per outer row
18522        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18523        // licences the correlation even without the keyword. Absorb
18524        // it and fall through to the SRF arms below.
18525        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18526        // just the four builtin SRFs: a user set-returning function on a JOIN's
18527        // right side is the whole point of LATERAL. The keyword stays noise at
18528        // parse time — the join executor substitutes the outer row into the
18529        // call's arguments per outer row.
18530        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18531            && matches!(
18532                self.tokens.get(self.pos + 1),
18533                // The json_each family has its OWN `LATERAL …` arm below, which
18534                // needs to see the keyword — absorbing it here would send those
18535                // calls down the generic table-function channel instead.
18536                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18537            )
18538            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18539        {
18540            self.advance(); // LATERAL
18541        }
18542        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18543        // set-returning function whose argument may reference a
18544        // preceding FROM item. We rewrite this to
18545        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18546        // AS __srf__) AS <alias>` so the existing LATERAL subquery
18547        // executor handles per-outer-row evaluation and the
18548        // SRF-primary jsonb_each_text path handles the inner
18549        // materialisation. Sentori 0067 backfill is the dogfood
18550        // shape.
18551        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18552            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18553            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18554        {
18555            self.advance(); // LATERAL
18556            let each_fn = match self.peek() {
18557                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18558                _ => unreachable!(),
18559            };
18560            self.advance(); // jsonb_each[_text] / json_each[_text]
18561            self.advance(); // (
18562            let arg = self.parse_expr(0)?;
18563            if !matches!(self.peek(), Token::RParen) {
18564                return Err(self.err(alloc::format!(
18565                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18566                    self.peek()
18567                )));
18568            }
18569            self.advance();
18570            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18571            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18572            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18573            //               FROM jsonb_each_text(<arg>) AS __srf__
18574            // PG's `AS kv(key, value)` column-alias list maps
18575            // positions to names; default to (key, value) when
18576            // omitted (matching the SRF's natural column names).
18577            let srf_alias = "__srf__".to_string();
18578            let key_alias = column_aliases
18579                .first()
18580                .cloned()
18581                .unwrap_or_else(|| "key".to_string());
18582            let value_alias = column_aliases
18583                .get(1)
18584                .cloned()
18585                .unwrap_or_else(|| "value".to_string());
18586            let inner_select = crate::ast::SelectStatement {
18587                locking: None,
18588                ctes: Vec::new(),
18589                distinct: false,
18590                distinct_on: Vec::new(),
18591                items: alloc::vec![
18592                    crate::ast::SelectItem::Expr {
18593                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18594                            qualifier: Some(srf_alias.clone()),
18595                            name: "key".to_string(),
18596                        }),
18597                        alias: Some(key_alias),
18598                    },
18599                    crate::ast::SelectItem::Expr {
18600                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18601                            qualifier: Some(srf_alias.clone()),
18602                            name: "value".to_string(),
18603                        }),
18604                        alias: Some(value_alias),
18605                    },
18606                ],
18607                from: Some(crate::ast::FromClause {
18608                    primary: TableRef {
18609                        name: srf_alias.clone(),
18610                        alias: Some(srf_alias.clone()),
18611                        only: false,
18612                        as_of_segment: None,
18613                        unnest_expr: None,
18614                        unnest_column_aliases: Vec::new(),
18615                        with_ordinality: false,
18616                        generate_series_args: None,
18617                        lateral_subquery: None,
18618                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18619                        table_fn_call: None,
18620                        rows_from: None,
18621                        json_table: None,
18622                        scalar_fn_item: false,
18623                    },
18624                    joins: Vec::new(),
18625                }),
18626                where_: None,
18627                group_by: None,
18628                group_by_all: false,
18629                having: None,
18630                unions: Vec::new(),
18631                order_by: Vec::new(),
18632                limit: None,
18633                offset: None,
18634                limit_with_ties: false,
18635                window_check_exprs: Vec::new(),
18636            };
18637            return Ok(TableRef {
18638                name: alias.clone(),
18639                alias: Some(alias),
18640                only: false,
18641                as_of_segment: None,
18642                unnest_expr: None,
18643                unnest_column_aliases: Vec::new(),
18644                with_ordinality: false,
18645                generate_series_args: None,
18646                lateral_subquery: Some(Box::new(inner_select)),
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 — bare `CROSS JOIN jsonb_each_text(t.col)`
18655        // without an explicit `LATERAL` keyword is the same shape
18656        // PG accepts (SRF naturally licences lateral correlation).
18657        // We mirror the LATERAL rewrite when the argument syntactic-
18658        // ally references an outer column (Column { qualifier:
18659        // Some(_), … }). For simplicity we apply the rewrite
18660        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18661        // in the FROM-list — caller-side join parsing positions
18662        // this peek correctly.
18663        // (Implementation note: detection lives below; the LATERAL
18664        // branch above already covers the explicit form; the bare
18665        // form falls through to the plain SRF arm and the engine
18666        // treats it as a constant-arg SRF if no outer reference is
18667        // present.)
18668        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18669        // table. Detect at the head so it claims precedence over
18670        // every other table-ref shape (unnest / generate_series /
18671        // bare ident); the lateral subquery itself follows the
18672        // regular SELECT grammar.
18673        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18674        // t(cols)`. Each row lowers to a constant SELECT with PG's
18675        // default column1..columnN names; subsequent rows chain as
18676        // UNION ALL peers. The result rides the derived-table
18677        // lateral_subquery channel — zero executor work.
18678        if matches!(self.peek(), Token::LParen)
18679            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18680        {
18681            self.advance(); // (
18682            self.advance(); // VALUES
18683            let head = self.parse_values_rows_body()?;
18684            if !matches!(self.peek(), Token::RParen) {
18685                return Err(self.err(alloc::format!(
18686                    "expected ')' after VALUES list, got {:?}",
18687                    self.peek()
18688                )));
18689            }
18690            self.advance();
18691            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18692            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18693            return Ok(TableRef {
18694                name,
18695                alias: alias_ident,
18696                only: false,
18697                as_of_segment: None,
18698                unnest_expr: None,
18699                unnest_column_aliases: column_aliases,
18700                with_ordinality: false,
18701                generate_series_args: None,
18702                lateral_subquery: Some(Box::new(head)),
18703                jsonb_each_text_arg: None,
18704                table_fn_call: None,
18705                rows_from: None,
18706                json_table: None,
18707                scalar_fn_item: false,
18708            });
18709        }
18710        // v7.37.17 (17.6 siblings) — plain derived table:
18711        // `FROM ( SELECT … ) [AS] alias`. Rides the same
18712        // lateral_subquery channel the explicit LATERAL form uses —
18713        // an uncorrelated inner SELECT executes identically. The
18714        // inner parse carries UNION tails (they live on
18715        // SelectStatement.unions).
18716        // v7.37 D.20 — the derived-table inner may itself be a
18717        // parenthesized set-operation group (`FROM ((SELECT…) UNION
18718        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18719        // bare `(SELECT …)`. parse_one_statement already routes a leading
18720        // `(` set-op group (its LParen arm) and a leading WITH
18721        // (parse_with_cte_then_select), so widen the second-token gate to
18722        // Select | LParen | WITH.
18723        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18724        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18725        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18726        // has existed since the shorthand landed and `parse_bare_select`
18727        // already routes it ("valid anywhere a SELECT head is"); what was
18728        // missing is this second-token gate, and the CTE body's dispatch
18729        // below. Round 868 found both by putting the shorthand in a
18730        // subquery — the top-level forms had been the only ones tested.
18731        if matches!(self.peek(), Token::LParen)
18732            && (matches!(
18733                self.tokens.get(self.pos + 1),
18734                Some(Token::Select | Token::LParen | Token::Table)
18735            ) || matches!(self.tokens.get(self.pos + 1),
18736                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18737        {
18738            self.advance(); // (
18739            let inner = match self.parse_one_statement()? {
18740                Statement::Select(s) => s,
18741                other => {
18742                    return Err(self.err(alloc::format!(
18743                        "expected SELECT inside derived table ( … ), got {other:?}"
18744                    )));
18745                }
18746            };
18747            if !matches!(self.peek(), Token::RParen) {
18748                return Err(self.err(alloc::format!(
18749                    "expected ')' after derived-table subquery, got {:?}",
18750                    self.peek()
18751                )));
18752            }
18753            self.advance();
18754            // `AS t(a, b)` column-alias list rides the
18755            // unnest_column_aliases field (same positional-rename
18756            // contract the unnest SRFs use).
18757            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18758            let name = alias_ident
18759                .clone()
18760                .unwrap_or_else(|| "subquery".to_string());
18761            return Ok(TableRef {
18762                name,
18763                alias: alias_ident,
18764                only: false,
18765                as_of_segment: None,
18766                unnest_expr: None,
18767                unnest_column_aliases: column_aliases,
18768                with_ordinality: false,
18769                generate_series_args: None,
18770                lateral_subquery: Some(Box::new(inner)),
18771                jsonb_each_text_arg: None,
18772                table_fn_call: None,
18773                rows_from: None,
18774                json_table: None,
18775                scalar_fn_item: false,
18776            });
18777        }
18778        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18779            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18780        {
18781            self.advance(); // LATERAL
18782            self.advance(); // (
18783            // Parse the inner SELECT.
18784            let inner = match self.parse_one_statement()? {
18785                Statement::Select(s) => s,
18786                other => {
18787                    return Err(self.err(alloc::format!(
18788                        "expected SELECT inside LATERAL ( … ), got {other:?}"
18789                    )));
18790                }
18791            };
18792            if !matches!(self.peek(), Token::RParen) {
18793                return Err(self.err(alloc::format!(
18794                    "expected ')' after LATERAL subquery, got {:?}",
18795                    self.peek()
18796                )));
18797            }
18798            self.advance();
18799            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
18800            // `(VALUES …) t(g)` derived table round-trips through view-body
18801            // Display, which renders on the lateral_subquery channel).
18802            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18803            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
18804            return Ok(TableRef {
18805                name,
18806                alias: alias_ident,
18807                only: false,
18808                as_of_segment: None,
18809                unnest_expr: None,
18810                unnest_column_aliases: column_aliases,
18811                with_ordinality: false,
18812                generate_series_args: None,
18813                lateral_subquery: Some(Box::new(inner)),
18814                jsonb_each_text_arg: None,
18815                table_fn_call: None,
18816                rows_from: None,
18817                json_table: None,
18818                scalar_fn_item: false,
18819            });
18820        }
18821        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
18822        // function as a FROM item. Emits one row per (key, value)
18823        // pair in the JSONB object argument as TEXT columns. May
18824        // be wrapped in CROSS JOIN LATERAL when the argument
18825        // references a preceding FROM item (sentori migration
18826        // 0067 backfill shape: `CROSS JOIN LATERAL
18827        // jsonb_each_text(t.json_col) AS kv(key, value)`).
18828        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
18829            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18830        {
18831            let each_fn = match self.peek() {
18832                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18833                _ => unreachable!(),
18834            };
18835            self.advance(); // jsonb_each[_text] / json_each[_text]
18836            self.advance(); // (
18837            let arg = self.parse_expr(0)?;
18838            if !matches!(self.peek(), Token::RParen) {
18839                return Err(self.err(alloc::format!(
18840                    "expected ')' after {each_fn}() argument, got {:?}",
18841                    self.peek()
18842                )));
18843            }
18844            self.advance();
18845            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18846            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18847            return Ok(TableRef {
18848                name,
18849                alias: alias_ident,
18850                only: false,
18851                as_of_segment: None,
18852                unnest_expr: None,
18853                // `AS t(k, v)` renames key/value positionally, same as the
18854                // LATERAL-position form already does.
18855                unnest_column_aliases: column_aliases,
18856                with_ordinality: false,
18857                generate_series_args: None,
18858                lateral_subquery: None,
18859                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18860                table_fn_call: None,
18861                rows_from: None,
18862                json_table: None,
18863                scalar_fn_item: false,
18864            });
18865        }
18866        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
18867        // (+ json_ variants) — record-returning JSON functions with a
18868        // column-definition list. Desugar to a derived table that
18869        // projects each declared column from the JSON via `->>` + a cast,
18870        // over `jsonb_array_elements(J)` for the *set (per-element) form.
18871        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
18872            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18873        {
18874            return self.parse_json_to_record_from();
18875        }
18876        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
18877        // row is a text[] of capture groups, so it cannot desugar to unnest
18878        // (that would flatten the array). Wrap it as a derived table
18879        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
18880        // SRF path already emits one text[] row per match. PG names the column
18881        // `regexp_matches`; an `AS a(col)` alias overrides it.
18882        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18883                if s.eq_ignore_ascii_case("regexp_matches"))
18884            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18885        {
18886            self.advance(); // fn name
18887            self.advance(); // (
18888            let mut fn_args: Vec<Expr> = Vec::new();
18889            loop {
18890                fn_args.push(self.parse_expr(0)?);
18891                if matches!(self.peek(), Token::Comma) {
18892                    self.advance();
18893                    continue;
18894                }
18895                break;
18896            }
18897            if !matches!(self.peek(), Token::RParen) {
18898                return Err(self.err(alloc::format!(
18899                    "expected ')' after regexp_matches() arguments, got {:?}",
18900                    self.peek()
18901                )));
18902            }
18903            self.advance();
18904            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
18905            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
18906            // it, so it died on the `with` token while every other table function
18907            // accepted it.
18908            let with_ordinality = self.absorb_with_ordinality();
18909            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18910            let table_alias = alias_ident
18911                .clone()
18912                .unwrap_or_else(|| "regexp_matches".to_string());
18913            // PG names a single-column function's output column after the ALIAS
18914            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
18915            // `m` reads as that column and not as a whole-row composite. Naming
18916            // it after the function regardless made `SELECT m[1] FROM … AS m`
18917            // subscript a record.
18918            let col_name = column_aliases
18919                .first()
18920                .cloned()
18921                .or_else(|| alias_ident.clone())
18922                .unwrap_or_else(|| "regexp_matches".to_string());
18923            let inner = crate::ast::SelectStatement {
18924                locking: None,
18925                ctes: Vec::new(),
18926                distinct: false,
18927                distinct_on: Vec::new(),
18928                items: alloc::vec![SelectItem::Expr {
18929                    expr: Expr::FunctionCall {
18930                        name: "regexp_matches".to_string(),
18931                        args: fn_args,
18932                    },
18933                    alias: Some(col_name),
18934                }],
18935                from: None,
18936                where_: None,
18937                group_by: None,
18938                group_by_all: false,
18939                having: None,
18940                unions: Vec::new(),
18941                order_by: Vec::new(),
18942                limit: None,
18943                offset: None,
18944                limit_with_ties: false,
18945                window_check_exprs: Vec::new(),
18946            };
18947            return Ok(TableRef {
18948                name: table_alias.clone(),
18949                alias: Some(table_alias),
18950                only: false,
18951                as_of_segment: None,
18952                unnest_expr: None,
18953                unnest_column_aliases: column_aliases,
18954                with_ordinality,
18955                generate_series_args: None,
18956                lateral_subquery: Some(Box::new(inner)),
18957                jsonb_each_text_arg: None,
18958                table_fn_call: None,
18959                rows_from: None,
18960                json_table: None,
18961                // regexp_matches returns text[], a base type: `SELECT m FROM
18962                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
18963                scalar_fn_item: true,
18964            });
18965        }
18966        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
18967        // / json_ variants as a FROM item. Rewritten into
18968        // `unnest(<same fn>(<expr>))`: the scalar form returns the
18969        // elements as a TEXT array, and the existing unnest SRF path
18970        // materialises one row per element. PG's natural column name
18971        // is `value`; an `AS a(col)` column-alias list overrides it.
18972        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18973                if s.eq_ignore_ascii_case("jsonb_array_elements")
18974                    || s.eq_ignore_ascii_case("json_array_elements")
18975                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
18976                    || s.eq_ignore_ascii_case("json_array_elements_text")
18977                    || s.eq_ignore_ascii_case("jsonb_object_keys")
18978                    || s.eq_ignore_ascii_case("json_object_keys")
18979                    || s.eq_ignore_ascii_case("jsonb_path_query")
18980                    || s.eq_ignore_ascii_case("json_path_query")
18981                    || s.eq_ignore_ascii_case("generate_subscripts")
18982                    || s.eq_ignore_ascii_case("string_to_table")
18983                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
18984            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18985        {
18986            let fn_name = match self.peek() {
18987                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18988                _ => unreachable!(),
18989            };
18990            self.advance(); // fn name
18991            self.advance(); // (
18992            let mut fn_args: Vec<Expr> = Vec::new();
18993            loop {
18994                fn_args.push(self.parse_expr(0)?);
18995                if matches!(self.peek(), Token::Comma) {
18996                    self.advance();
18997                    continue;
18998                }
18999                break;
19000            }
19001            if !matches!(self.peek(), Token::RParen) {
19002                return Err(self.err(alloc::format!(
19003                    "expected ')' after {fn_name}() arguments, got {:?}",
19004                    self.peek()
19005                )));
19006            }
19007            self.advance();
19008            let with_ordinality = self.absorb_with_ordinality();
19009            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19010            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19011            // PG's natural column name: the array-elements SRFs
19012            // declare an OUT parameter `value`; jsonb_object_keys
19013            // and generate_subscripts have none, so the column is
19014            // named after the function. A bare table alias on a
19015            // single-column SRF renames the column too (PG: `FROM
19016            // generate_subscripts(a, 1) AS s` projects column s) —
19017            // except for the OUT-parameter SRFs, whose column stays
19018            // `value` under a bare alias.
19019            let natural_col = if fn_name.ends_with("_array_elements")
19020                || fn_name.ends_with("_array_elements_text")
19021            {
19022                "value".to_string()
19023            } else {
19024                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19025            };
19026            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19027            // Keep any further entries — the second names the
19028            // ordinality column under WITH ORDINALITY.
19029            srf_cols.extend(column_aliases.into_iter().skip(1));
19030            // The *_to_table SRFs are row-streams over the existing
19031            // *_to_array scalars — map the call target; the display
19032            // name (alias / column defaults) keeps the SRF spelling.
19033            let call_name = match fn_name.as_str() {
19034                "string_to_table" => "string_to_array".to_string(),
19035                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19036                _ => fn_name,
19037            };
19038            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19039            // preceding FROM item (bare or qualified column) is correlated;
19040            // route it through the per-outer-row lateral channel.
19041            let expr = crate::ast::Expr::FunctionCall {
19042                name: call_name,
19043                args: fn_args,
19044            };
19045            let correlated = Self::expr_has_any_column(&expr);
19046            let tref = TableRef {
19047                name,
19048                alias: alias_ident,
19049                only: false,
19050                as_of_segment: None,
19051                unnest_expr: Some(Box::new(expr)),
19052                unnest_column_aliases: srf_cols,
19053                with_ordinality,
19054                generate_series_args: None,
19055                lateral_subquery: None,
19056                jsonb_each_text_arg: None,
19057                table_fn_call: None,
19058                rows_from: None,
19059                json_table: None,
19060                // Each of these returns a BASE type (jsonb / text / int), so the item's
19061                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19062                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19063                scalar_fn_item: !with_ordinality,
19064            };
19065            return Ok(if correlated {
19066                Self::wrap_correlated_srf(tref)
19067            } else {
19068                tref
19069            });
19070        }
19071        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19072        // explicit parallel-zip syntax. Each entry lowers to its
19073        // array-returning scalar form (unnest(x) → x itself; the
19074        // FROM-SRF rewrite family → their scalar array calls) and
19075        // the list rides the multi-arg unnest zip channel:
19076        // NULL-padded to the longest, WITH ORDINALITY appends the
19077        // counter. generate_series has no scalar array form and
19078        // errors honestly.
19079        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19080            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19081            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19082        {
19083            self.advance(); // ROWS
19084            self.advance(); // FROM
19085            self.advance(); // (
19086            let mut entries: Vec<Expr> = Vec::new();
19087            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19088            // Used only when some entry has no array form.
19089            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19090            loop {
19091                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19092                if !matches!(self.peek(), Token::LParen) {
19093                    return Err(self.err(alloc::format!(
19094                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19095                        self.peek()
19096                    )));
19097                }
19098                self.advance();
19099                let mut fn_args: Vec<Expr> = Vec::new();
19100                if !matches!(self.peek(), Token::RParen) {
19101                    loop {
19102                        fn_args.push(self.parse_expr(0)?);
19103                        if matches!(self.peek(), Token::Comma) {
19104                            self.advance();
19105                            continue;
19106                        }
19107                        break;
19108                    }
19109                }
19110                if !matches!(self.peek(), Token::RParen) {
19111                    return Err(self.err(alloc::format!(
19112                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19113                        self.peek()
19114                    )));
19115                }
19116                self.advance();
19117                let entry = match fn_name.as_str() {
19118                    "unnest" => {
19119                        if fn_args.len() != 1 {
19120                            return Err(
19121                                self.err("unnest inside ROWS FROM takes exactly one array".into())
19122                            );
19123                        }
19124                        fn_args.pop().expect("len checked")
19125                    }
19126                    "jsonb_array_elements"
19127                    | "json_array_elements"
19128                    | "jsonb_array_elements_text"
19129                    | "json_array_elements_text"
19130                    | "jsonb_object_keys"
19131                    | "json_object_keys"
19132                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19133                        name: fn_name,
19134                        args: fn_args,
19135                    },
19136                    "string_to_table" => crate::ast::Expr::FunctionCall {
19137                        name: "string_to_array".to_string(),
19138                        args: fn_args,
19139                    },
19140                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19141                        name: "regexp_split_to_array".to_string(),
19142                        args: fn_args,
19143                    },
19144                    // v7.39 (read01 round 74) — an SRF with no array form
19145                    // (`generate_series`, a user `RETURNS SETOF` function) has no
19146                    // scalar expression to zip, so the WHOLE list switches to the
19147                    // rows_from channel, which runs each function and zips the
19148                    // rows themselves. The all-array case keeps the old lowering:
19149                    // it is well-trodden and this must not disturb it.
19150                    _ => {
19151                        generic.push((fn_name, fn_args));
19152                        if matches!(self.peek(), Token::Comma) {
19153                            self.advance();
19154                            continue;
19155                        }
19156                        break;
19157                    }
19158                };
19159                generic.push((
19160                    // The array-able entries carry their lowered expr along, so a
19161                    // MIXED list still works: the engine sees the scalar array
19162                    // form and unnests it.
19163                    "__array".to_string(),
19164                    alloc::vec![entry.clone()],
19165                ));
19166                entries.push(entry);
19167                if matches!(self.peek(), Token::Comma) {
19168                    self.advance();
19169                    continue;
19170                }
19171                break;
19172            }
19173            if !matches!(self.peek(), Token::RParen) {
19174                return Err(self.err(alloc::format!(
19175                    "expected ')' to close ROWS FROM, got {:?}",
19176                    self.peek()
19177                )));
19178            }
19179            self.advance();
19180            let with_ordinality = self.absorb_with_ordinality();
19181            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19182            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19183            // v7.39 (read01 round 74) — some entry had no array form, so the whole
19184            // list rides the generic channel.
19185            if generic.iter().any(|(n, _)| n != "__array") {
19186                let correlated = generic
19187                    .iter()
19188                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19189                let tref = TableRef {
19190                    name,
19191                    alias: alias_ident,
19192                    only: false,
19193                    as_of_segment: None,
19194                    unnest_expr: None,
19195                    unnest_column_aliases,
19196                    with_ordinality,
19197                    generate_series_args: None,
19198                    lateral_subquery: None,
19199                    jsonb_each_text_arg: None,
19200                    table_fn_call: None,
19201                    rows_from: Some(generic),
19202                    json_table: None,
19203                    scalar_fn_item: false,
19204                };
19205                return Ok(if correlated {
19206                    Self::wrap_correlated_srf(tref)
19207                } else {
19208                    tref
19209                });
19210            }
19211            let correlated = entries.iter().any(Self::expr_has_any_column);
19212            let expr = if entries.len() == 1 {
19213                entries.pop().expect("len checked")
19214            } else {
19215                crate::ast::Expr::FunctionCall {
19216                    name: "__unnest_zip".to_string(),
19217                    args: entries,
19218                }
19219            };
19220            let tref = TableRef {
19221                name,
19222                alias: alias_ident,
19223                only: false,
19224                as_of_segment: None,
19225                unnest_expr: Some(Box::new(expr)),
19226                unnest_column_aliases,
19227                with_ordinality,
19228                generate_series_args: None,
19229                lateral_subquery: None,
19230                jsonb_each_text_arg: None,
19231                table_fn_call: None,
19232                rows_from: None,
19233                json_table: None,
19234                scalar_fn_item: false,
19235            };
19236            return Ok(if correlated {
19237                Self::wrap_correlated_srf(tref)
19238            } else {
19239                tref
19240            });
19241        }
19242        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19243        // source. Detect at the head before the bare-ident fallback;
19244        // unnest is not a reserved token.
19245        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19246            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19247        {
19248            self.advance(); // unnest
19249            self.advance(); // (
19250            let mut srf_args = alloc::vec![self.parse_expr(0)?];
19251            while matches!(self.peek(), Token::Comma) {
19252                self.advance();
19253                srf_args.push(self.parse_expr(0)?);
19254            }
19255            if !matches!(self.peek(), Token::RParen) {
19256                return Err(self.err(alloc::format!(
19257                    "expected ')' after unnest() argument, got {:?}",
19258                    self.peek()
19259                )));
19260            }
19261            self.advance();
19262            // Multi-arg unnest(a, b, …) zips the arrays in
19263            // parallel, NULL-padding to the longest (PG's ROWS
19264            // FROM shorthand). Lower onto the unnest channel as an
19265            // internal marker call the executors unpack.
19266            let expr = if srf_args.len() == 1 {
19267                srf_args.pop().expect("len checked")
19268            } else {
19269                crate::ast::Expr::FunctionCall {
19270                    name: "__unnest_zip".to_string(),
19271                    args: srf_args,
19272                }
19273            };
19274            let with_ordinality = self.absorb_with_ordinality();
19275            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19276            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19277            let correlated = Self::expr_has_any_column(&expr);
19278            let tref = TableRef {
19279                name,
19280                alias: alias_ident,
19281                only: false,
19282                as_of_segment: None,
19283                unnest_expr: Some(Box::new(expr)),
19284                unnest_column_aliases,
19285                with_ordinality,
19286                generate_series_args: None,
19287                lateral_subquery: None,
19288                jsonb_each_text_arg: None,
19289                table_fn_call: None,
19290                rows_from: None,
19291                json_table: None,
19292                scalar_fn_item: false,
19293            };
19294            return Ok(if correlated {
19295                Self::wrap_correlated_srf(tref)
19296            } else {
19297                tref
19298            });
19299        }
19300        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19301        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19302        // generic table-fn arg parser can't read), so it is intercepted
19303        // here BEFORE the generic dispatch. The doc expr may reference
19304        // outer columns (implicit LATERAL) — same correlated-wrap rule.
19305        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19306                if s.eq_ignore_ascii_case("json_table"))
19307            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19308        {
19309            let tref = self.parse_json_table_ref()?;
19310            let correlated = tref
19311                .json_table
19312                .as_deref()
19313                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19314            return Ok(if correlated {
19315                Self::wrap_correlated_srf(tref)
19316            } else {
19317                tref
19318            });
19319        }
19320        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19321        // functions dispatched by name (`pg_partition_tree('t')`,
19322        // `pg_partition_ancestors('t')`). Same head-detection shape as
19323        // unnest; the engine executor owns the row shape per function.
19324        // v7.39 (read01 round 65) — and a USER function in FROM position
19325        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19326        // (generate_series / unnest / the json_each family) keep it — their arms
19327        // sit further down, so they are excluded here by name rather than by
19328        // ordering. Anything else that is an ident followed by `(` is a table
19329        // function; the engine executor decides whether it is a builtin, a
19330        // set-returning user function, or an error.
19331        // 7.38.1 S5.1 — pg_dump spells its table functions
19332        // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
19333        // strip the pg_catalog prefix here so the same head-detection
19334        // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
19335        // meaning.
19336        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
19337            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19338            && matches!(
19339                self.tokens.get(self.pos + 2),
19340                Some(Token::Ident(_) | Token::QuotedIdent(_))
19341            )
19342            && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
19343        {
19344            self.advance(); // pg_catalog
19345            self.advance(); // .
19346        }
19347        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19348                if !s.eq_ignore_ascii_case("generate_series")
19349                    && !s.eq_ignore_ascii_case("unnest")
19350                    && !is_json_each_name(s))
19351            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19352        {
19353            // Body out-of-line — this parse sits on the FROM/subquery
19354            // recursion chain (debug frame-cliff discipline).
19355            // v7.39 (read01 round 69) — a call whose arguments reference an outer
19356            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19357            // outer row, so it rides the lateral channel. Same rule the unnest
19358            // arm uses.
19359            let tref = self.parse_table_fn_ref()?;
19360            let correlated = tref
19361                .table_fn_call
19362                .as_deref()
19363                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19364            return Ok(if correlated {
19365                Self::wrap_correlated_srf(tref)
19366            } else {
19367                tref
19368            });
19369        }
19370        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19371        // [, step])` set-returning source. Same shape as unnest:
19372        // detect at the head, parse the comma-separated arg list,
19373        // dispatch downstream through the engine's set-returning
19374        // path. Supports integer triplets (mailrs's `WITH row_no AS
19375        // (SELECT * FROM generate_series(1, N))` pattern) and
19376        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19377        // date-range iteration pattern, which pre-3.10 had no
19378        // direct equivalent in SPG).
19379        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19380            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19381        {
19382            self.advance(); // generate_series
19383            self.advance(); // (
19384            let mut args: Vec<Expr> = Vec::new();
19385            loop {
19386                args.push(self.parse_expr(0)?);
19387                if matches!(self.peek(), Token::Comma) {
19388                    self.advance();
19389                    continue;
19390                }
19391                break;
19392            }
19393            if !matches!(self.peek(), Token::RParen) {
19394                return Err(self.err(alloc::format!(
19395                    "expected ')' after generate_series() arguments, got {:?}",
19396                    self.peek()
19397                )));
19398            }
19399            self.advance();
19400            if args.len() < 2 || args.len() > 3 {
19401                return Err(self.err(alloc::format!(
19402                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19403                    args.len()
19404                )));
19405            }
19406            let with_ordinality = self.absorb_with_ordinality();
19407            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19408            let name = alias_ident
19409                .clone()
19410                .unwrap_or_else(|| "generate_series".to_string());
19411            let correlated = args.iter().any(Self::expr_has_any_column);
19412            let tref = TableRef {
19413                name,
19414                alias: alias_ident,
19415                only: false,
19416                as_of_segment: None,
19417                unnest_expr: None,
19418                unnest_column_aliases: column_aliases,
19419                with_ordinality,
19420                generate_series_args: Some(args),
19421                lateral_subquery: None,
19422                jsonb_each_text_arg: None,
19423                table_fn_call: None,
19424                rows_from: None,
19425                json_table: None,
19426                scalar_fn_item: false,
19427            };
19428            return Ok(if correlated {
19429                Self::wrap_correlated_srf(tref)
19430            } else {
19431                tref
19432            });
19433        }
19434        // v7.16.2 — preserve information_schema / pg_catalog
19435        // qualifiers (mailrs round-10 A.3). The generic
19436        // `expect_ident_like` strip silently drops the schema;
19437        // we want the engine to recognise these PG meta tables
19438        // and synthesise rows from the live catalog. Produce a
19439        // synthetic name (`__spg_info_columns` etc.) so the
19440        // engine's SELECT-side router can dispatch without
19441        // clashing with any user-defined `columns` table.
19442        let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
19443            (synth, Some(orig))
19444        } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
19445            (synth, Some(orig))
19446        } else {
19447            (self.expect_ident_like()?, None)
19448        };
19449        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19450        // time-travel clause. Parse BEFORE the alias so the
19451        // alias can still ride at the tail (`tbl AS OF SEGMENT
19452        // '5' alias`). `AS` is a reserved keyword token, while
19453        // `OF` and `SEGMENT` are bare idents.
19454        let as_of_segment = if matches!(self.peek(), Token::As)
19455            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19456        {
19457            self.advance(); // AS
19458            self.advance(); // OF
19459            let kw = match self.peek().clone() {
19460                Token::Ident(s) | Token::QuotedIdent(s) => s,
19461                other => {
19462                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19463                }
19464            };
19465            if !kw.eq_ignore_ascii_case("segment") {
19466                return Err(self.err(format!(
19467                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19468                )));
19469            }
19470            self.advance();
19471            // Segment id literal — accept either a string or
19472            // integer for operator ergonomics.
19473            let id = match self.advance() {
19474                Token::String(s) => s
19475                    .parse::<u32>()
19476                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19477                Token::Integer(n) => u32::try_from(n)
19478                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19479                other => {
19480                    return Err(self.err(format!(
19481                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
19482                    )));
19483                }
19484            };
19485            Some(id)
19486        } else {
19487            None
19488        };
19489        // TABLESAMPLE is not a reserved token — keep the bare-ident
19490        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19491        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19492        {
19493            None
19494        } else {
19495            self.parse_optional_alias()?
19496        };
19497        // r1052 — a catalog name rewritten to its synthetic form keeps
19498        // the WRITTEN name as the relation's alias, so `pg_cast.oid`
19499        // still binds after `pg_cast` became `__spg_pg_cast`. PG
19500        // semantics: the visible name of `pg_catalog.pg_cast` IS
19501        // `pg_cast`. Without this, every table-name-qualified column
19502        // on a synthesised catalog answered "missing FROM-clause
19503        // entry" — which is the wall pg_dump hit on its first
19504        // pg_proc/pg_cast query.
19505        let alias = match (&alias, &meta_original) {
19506            (None, Some(orig)) if *orig != name => Some(orig.clone()),
19507            _ => alias,
19508        };
19509        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19510        // (PG grammar). BERNOULLI lowers to a per-row
19511        // `random() < p/100` conjunct on the enclosing SELECT's
19512        // WHERE — exact row-level Bernoulli semantics. SYSTEM
19513        // shares the lowering: SPG has no page structure to
19514        // sample, and the row-level form returns the same expected
19515        // fraction. REPEATABLE(seed) promises a deterministic
19516        // sample SPG cannot honour yet — honest error rather than
19517        // a silently ignored seed.
19518        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19519            self.advance();
19520            let method = self.expect_ident_like()?;
19521            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19522                return Err(self.err(alloc::format!(
19523                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19524                )));
19525            }
19526            if !matches!(self.peek(), Token::LParen) {
19527                return Err(self.err(alloc::format!(
19528                    "expected '(' after TABLESAMPLE {}, got {:?}",
19529                    method.to_ascii_uppercase(),
19530                    self.peek()
19531                )));
19532            }
19533            self.advance();
19534            let percent = self.parse_expr(0)?;
19535            if !matches!(self.peek(), Token::RParen) {
19536                return Err(self.err(alloc::format!(
19537                    "expected ')' after TABLESAMPLE percentage, got {:?}",
19538                    self.peek()
19539                )));
19540            }
19541            self.advance();
19542            // REPEATABLE(seed) → a deterministic per-row draw seeded by
19543            // `seed`, so the sample is stable across repeats and rescans.
19544            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19545            let mut sample_seed: Option<Expr> = None;
19546            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19547                self.advance();
19548                if !matches!(self.peek(), Token::LParen) {
19549                    return Err(self.err(alloc::format!(
19550                        "expected '(' after REPEATABLE, got {:?}",
19551                        self.peek()
19552                    )));
19553                }
19554                self.advance();
19555                let seed = self.parse_expr(0)?;
19556                if !matches!(self.peek(), Token::RParen) {
19557                    return Err(self.err(alloc::format!(
19558                        "expected ')' after REPEATABLE seed, got {:?}",
19559                        self.peek()
19560                    )));
19561                }
19562                self.advance();
19563                sample_seed = Some(seed);
19564            }
19565            let draw = match sample_seed {
19566                Some(seed) => Expr::FunctionCall {
19567                    name: "__tsm_fract".to_string(),
19568                    args: alloc::vec![seed],
19569                },
19570                None => Expr::FunctionCall {
19571                    name: "random".to_string(),
19572                    args: Vec::new(),
19573                },
19574            };
19575            self.pending_sample_preds.push(Expr::Binary {
19576                lhs: Box::new(draw),
19577                op: crate::ast::BinOp::Lt,
19578                rhs: Box::new(Expr::Binary {
19579                    lhs: Box::new(percent),
19580                    op: crate::ast::BinOp::Div,
19581                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19582                }),
19583            });
19584        }
19585        Ok(TableRef {
19586            name,
19587            alias,
19588            only,
19589            as_of_segment,
19590            unnest_expr: None,
19591            unnest_column_aliases: Vec::new(),
19592            with_ordinality: false,
19593            generate_series_args: None,
19594            lateral_subquery: None,
19595            jsonb_each_text_arg: None,
19596            table_fn_call: None,
19597            rows_from: None,
19598            json_table: None,
19599            scalar_fn_item: false,
19600        })
19601    }
19602
19603    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19604    /// but also accepts `AS alias(col [, col, …])` — the
19605    /// PG-standard table-function column-list form. The column
19606    /// list is only honoured when paired with `UNNEST(...)` in
19607    /// the parent; other call sites currently discard it.
19608    /// True when the expression tree contains a qualified column
19609    /// reference (`t.col`) — the syntactic marker that an SRF
19610    /// argument correlates with a preceding FROM item.
19611    fn expr_has_qualified_column(e: &Expr) -> bool {
19612        match e {
19613            Expr::Column(c) => c.qualifier.is_some(),
19614            Expr::Binary { lhs, rhs, .. } => {
19615                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19616            }
19617            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19618            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19619            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19620            Expr::Case {
19621                operand,
19622                branches,
19623                else_branch,
19624            } => {
19625                operand
19626                    .as_deref()
19627                    .is_some_and(Self::expr_has_qualified_column)
19628                    || branches.iter().any(|(w, t)| {
19629                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19630                    })
19631                    || else_branch
19632                        .as_deref()
19633                        .is_some_and(Self::expr_has_qualified_column)
19634            }
19635            _ => false,
19636        }
19637    }
19638
19639    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19640    /// counts a bare (unqualified) column. A set-returning function has no
19641    /// input columns of its own, so ANY column in its arguments is an outer
19642    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19643    fn expr_has_any_column(e: &Expr) -> bool {
19644        match e {
19645            Expr::Column(_) => true,
19646            Expr::Binary { lhs, rhs, .. } => {
19647                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19648            }
19649            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19650            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19651            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19652            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19653            // constructor or subscript fell to the `_ => false` arm, so
19654            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19655            // channel and the eager peer eval answered `column "x" does
19656            // not exist` (the substitution walker already recurses both
19657            // shapes; only this detector was blind to them).
19658            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19659            Expr::ArraySubscript { target, index } => {
19660                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19661            }
19662            Expr::Case {
19663                operand,
19664                branches,
19665                else_branch,
19666            } => {
19667                operand.as_deref().is_some_and(Self::expr_has_any_column)
19668                    || branches
19669                        .iter()
19670                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19671                    || else_branch
19672                        .as_deref()
19673                        .is_some_and(Self::expr_has_any_column)
19674            }
19675            _ => false,
19676        }
19677    }
19678
19679    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19680    /// `generate_series(1, t.n)`) into the lateral_subquery
19681    /// channel: `SELECT * FROM <srf>` executes per outer row with
19682    /// outer references substituted (v7.37.43-T4.5 machinery).
19683    /// Uncorrelated SRFs stay on their plain channels.
19684    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19685        let name = srf.name.clone();
19686        let alias = srf.alias.clone();
19687        let inner = crate::ast::SelectStatement {
19688            locking: None,
19689            ctes: Vec::new(),
19690            distinct: false,
19691            distinct_on: Vec::new(),
19692            items: alloc::vec![crate::ast::SelectItem::Wildcard],
19693            from: Some(crate::ast::FromClause {
19694                primary: srf,
19695                joins: Vec::new(),
19696            }),
19697            where_: None,
19698            group_by: None,
19699            group_by_all: false,
19700            having: None,
19701            unions: Vec::new(),
19702            order_by: Vec::new(),
19703            limit: None,
19704            offset: None,
19705            limit_with_ties: false,
19706            window_check_exprs: Vec::new(),
19707        };
19708        TableRef {
19709            name,
19710            alias,
19711            only: false,
19712            as_of_segment: None,
19713            unnest_expr: None,
19714            unnest_column_aliases: Vec::new(),
19715            with_ordinality: false,
19716            generate_series_args: None,
19717            lateral_subquery: Some(Box::new(inner)),
19718            jsonb_each_text_arg: None,
19719            table_fn_call: None,
19720            rows_from: None,
19721            json_table: None,
19722            scalar_fn_item: false,
19723        }
19724    }
19725
19726    /// True when the expression tree contains an unresolved
19727    /// `OVER w` marker (see parse_over_clause).
19728    fn expr_has_named_window(e: &Expr) -> bool {
19729        match e {
19730            Expr::WindowFunction { partition_by, .. } => matches!(
19731                partition_by.as_slice(),
19732                [Expr::Column(c)] if matches!(
19733                    c.qualifier.as_deref(),
19734                    Some("__named_window__") | Some("__named_window_ref__")
19735                )
19736            ),
19737            Expr::Binary { lhs, rhs, .. } => {
19738                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19739            }
19740            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19741            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19742            Expr::Case {
19743                operand,
19744                branches,
19745                else_branch,
19746            } => {
19747                operand.as_deref().is_some_and(Self::expr_has_named_window)
19748                    || branches.iter().any(|(w, t)| {
19749                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19750                    })
19751                    || else_branch
19752                        .as_deref()
19753                        .is_some_and(Self::expr_has_named_window)
19754            }
19755            _ => false,
19756        }
19757    }
19758
19759    /// v7.39 (round 705) — the NAMES the expression references through the
19760    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19761    /// definitions nothing referenced. Traversal mirrors
19762    /// `expr_has_named_window` above.
19763    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19764        match e {
19765            Expr::WindowFunction { partition_by, .. } => {
19766                if let [Expr::Column(c)] = partition_by.as_slice()
19767                    && matches!(
19768                        c.qualifier.as_deref(),
19769                        Some("__named_window__") | Some("__named_window_ref__")
19770                    )
19771                {
19772                    into.push(c.name.clone());
19773                }
19774            }
19775            Expr::Binary { lhs, rhs, .. } => {
19776                Self::collect_named_window_refs(lhs, into);
19777                Self::collect_named_window_refs(rhs, into);
19778            }
19779            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19780                Self::collect_named_window_refs(expr, into);
19781            }
19782            Expr::FunctionCall { args, .. } => {
19783                for a in args {
19784                    Self::collect_named_window_refs(a, into);
19785                }
19786            }
19787            Expr::Case {
19788                operand,
19789                branches,
19790                else_branch,
19791            } => {
19792                if let Some(o) = operand.as_deref() {
19793                    Self::collect_named_window_refs(o, into);
19794                }
19795                for (w, t) in branches {
19796                    Self::collect_named_window_refs(w, into);
19797                    Self::collect_named_window_refs(t, into);
19798                }
19799                if let Some(eb) = else_branch.as_deref() {
19800                    Self::collect_named_window_refs(eb, into);
19801                }
19802            }
19803            _ => {}
19804        }
19805    }
19806
19807    /// Inline named-window definitions into the `OVER w` markers.
19808    /// An unknown name errors (PG: window "w" does not exist).
19809    #[allow(clippy::type_complexity)]
19810    fn substitute_named_windows(
19811        e: &mut Expr,
19812        defs: &[(
19813            String,
19814            (
19815                Vec<Expr>,
19816                Vec<(Expr, bool, Option<bool>)>,
19817                Option<WindowFrame>,
19818            ),
19819        )],
19820    ) -> Result<(), String> {
19821        match e {
19822            Expr::WindowFunction {
19823                partition_by,
19824                order_by,
19825                frame,
19826                ..
19827            } => {
19828                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
19829                // from the bare `OVER w1` (a plain reference).
19830                let named = match partition_by.as_slice() {
19831                    [Expr::Column(c)] => match c.qualifier.as_deref() {
19832                        Some("__named_window__") => Some((c.name.clone(), false)),
19833                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
19834                        _ => None,
19835                    },
19836                    _ => None,
19837                };
19838                if let Some((wname, is_copy)) = named {
19839                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
19840                    else {
19841                        return Err(alloc::format!("window {wname:?} does not exist"));
19842                    };
19843                    if !is_copy {
19844                        *partition_by = def.0.clone();
19845                        *order_by = def.1.clone();
19846                        *frame = def.2.clone();
19847                        return Ok(());
19848                    }
19849                    // v7.39 (round 229) — PG's copy rules, probed against
19850                    // 18.4: a copy inherits the partitioning, may supply an
19851                    // ordering only when the base has none, and may not copy
19852                    // a base that already carries a frame (its own frame
19853                    // would be ambiguous with the inherited one).
19854                    if !def.1.is_empty() && !order_by.is_empty() {
19855                        return Err(alloc::format!(
19856                            "cannot override ORDER BY clause of window \"{wname}\""
19857                        ));
19858                    }
19859                    if def.2.is_some() {
19860                        return Err(alloc::format!(
19861                            "cannot copy window \"{wname}\" because it has a frame clause"
19862                        ));
19863                    }
19864                    *partition_by = def.0.clone();
19865                    if order_by.is_empty() {
19866                        *order_by = def.1.clone();
19867                    }
19868                }
19869                Ok(())
19870            }
19871            Expr::Binary { lhs, rhs, .. } => {
19872                Self::substitute_named_windows(lhs, defs)?;
19873                Self::substitute_named_windows(rhs, defs)
19874            }
19875            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19876                Self::substitute_named_windows(expr, defs)
19877            }
19878            Expr::FunctionCall { args, .. } => {
19879                for a in args {
19880                    Self::substitute_named_windows(a, defs)?;
19881                }
19882                Ok(())
19883            }
19884            Expr::Case {
19885                operand,
19886                branches,
19887                else_branch,
19888            } => {
19889                if let Some(op) = operand {
19890                    Self::substitute_named_windows(op, defs)?;
19891                }
19892                for (w, t) in branches {
19893                    Self::substitute_named_windows(w, defs)?;
19894                    Self::substitute_named_windows(t, defs)?;
19895                }
19896                if let Some(el) = else_branch {
19897                    Self::substitute_named_windows(el, defs)?;
19898                }
19899                Ok(())
19900            }
19901            _ => Ok(()),
19902        }
19903    }
19904
19905    /// SQL-standard `TABLE name` shorthand — builds the equivalent
19906    /// `SELECT * FROM name` head. Callers own set-op chain / tail
19907    /// composition.
19908    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
19909        debug_assert!(matches!(self.peek(), Token::Table));
19910        self.advance(); // TABLE
19911        let tname = self.expect_ident_like()?;
19912        Ok(SelectStatement {
19913            locking: None,
19914            ctes: Vec::new(),
19915            distinct: false,
19916            distinct_on: Vec::new(),
19917            items: alloc::vec![SelectItem::Wildcard],
19918            from: Some(FromClause {
19919                primary: TableRef {
19920                    name: tname,
19921                    alias: None,
19922                    only: false,
19923                    as_of_segment: None,
19924                    unnest_expr: None,
19925                    unnest_column_aliases: Vec::new(),
19926                    with_ordinality: false,
19927                    generate_series_args: None,
19928                    lateral_subquery: None,
19929                    jsonb_each_text_arg: None,
19930                    table_fn_call: None,
19931                    rows_from: None,
19932                    json_table: None,
19933                    scalar_fn_item: false,
19934                },
19935                joins: Vec::new(),
19936            }),
19937            where_: None,
19938            group_by: None,
19939            group_by_all: false,
19940            having: None,
19941            unions: Vec::new(),
19942            order_by: Vec::new(),
19943            limit: None,
19944            offset: None,
19945            limit_with_ties: false,
19946            window_check_exprs: Vec::new(),
19947        })
19948    }
19949
19950    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
19951    /// variants) → a derived table that reads each declared column out of
19952    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
19953    /// `jsonb_array_elements(J)` (one row per element, column `value`);
19954    /// the scalar *record form projects a single row straight off `J`.
19955    /// Rides the existing lateral-subquery channel, so no new executor or
19956    /// AST is needed.
19957    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
19958        use crate::ast::{
19959            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
19960        };
19961        let fn_name = match self.peek() {
19962            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19963            _ => unreachable!("caller guarded is_json_to_record_name"),
19964        };
19965        self.advance(); // fn name
19966        self.advance(); // (
19967        let mut arg = self.parse_expr(0)?;
19968        // populate_record(base, json): the base only carries the record
19969        // type here — the JSON argument is the second expression.
19970        let mut base: Option<Expr> = None;
19971        if matches!(self.peek(), Token::Comma) {
19972            self.advance();
19973            base = Some(arg);
19974            arg = self.parse_expr(0)?;
19975        }
19976        if !matches!(self.peek(), Token::RParen) {
19977            return Err(self.err(alloc::format!(
19978                "expected ')' after {fn_name}() argument, got {:?}",
19979                self.peek()
19980            )));
19981        }
19982        self.advance(); // )
19983        let is_set = fn_name.ends_with("recordset");
19984        // `[AS] alias ( col type [, …] )` column-definition list.
19985        if matches!(self.peek(), Token::As) {
19986            self.advance();
19987        }
19988        let alias_opt = match self.peek() {
19989            Token::Ident(s) | Token::QuotedIdent(s) => {
19990                let a = s.clone();
19991                self.advance();
19992                Some(a)
19993            }
19994            _ => None,
19995        };
19996        // v7.39 (read01 round 76) — the populate family's canonical PG
19997        // spelling carries no column list at all: the row shape comes from
19998        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
19999        // j)`). The parser has no catalog, so hand the two arguments to the
20000        // engine's table-function channel, which does. Only `*_to_record*`
20001        // (whose base is bare `record`) genuinely requires the list.
20002        if !matches!(self.peek(), Token::LParen) {
20003            if let Some(base_expr) = base {
20004                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20005                return Ok(TableRef {
20006                    name: alias.clone(),
20007                    alias: Some(alias),
20008                    only: false,
20009                    as_of_segment: None,
20010                    unnest_expr: None,
20011                    unnest_column_aliases: Vec::new(),
20012                    with_ordinality: false,
20013                    generate_series_args: None,
20014                    lateral_subquery: None,
20015                    jsonb_each_text_arg: None,
20016                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20017                    rows_from: None,
20018                    json_table: None,
20019                    scalar_fn_item: false,
20020                });
20021            }
20022            return Err(self.err(alloc::format!(
20023                "expected '(' to start the {fn_name} column-definition list, got {:?}",
20024                self.peek()
20025            )));
20026        }
20027        let Some(alias) = alias_opt else {
20028            return Err(self.err(alloc::format!(
20029                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20030            )));
20031        };
20032        self.advance(); // (
20033        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20034        loop {
20035            let col = self.expect_ident_like()?;
20036            let ty = self.parse_cast_target()?;
20037            coldefs.push((col, ty));
20038            if matches!(self.peek(), Token::Comma) {
20039                self.advance();
20040                continue;
20041            }
20042            if matches!(self.peek(), Token::RParen) {
20043                self.advance();
20044                break;
20045            }
20046            return Err(self.err(alloc::format!(
20047                "expected ',' or ')' in {fn_name} column list, got {:?}",
20048                self.peek()
20049            )));
20050        }
20051        if coldefs.is_empty() {
20052            return Err(self.err(alloc::format!(
20053                "{fn_name} column-definition list must declare at least one column"
20054            )));
20055        }
20056        // Per column: (base ->> 'col')::type AS col. The base is the
20057        // per-element `value` column for the *set form, or the argument
20058        // itself for the scalar record form.
20059        let items: Vec<SelectItem> = coldefs
20060            .into_iter()
20061            .map(|(col, ty)| {
20062                let base = if is_set {
20063                    Expr::Column(ColumnName {
20064                        qualifier: None,
20065                        name: "value".to_string(),
20066                    })
20067                } else {
20068                    arg.clone()
20069                };
20070                SelectItem::Expr {
20071                    expr: Expr::Cast {
20072                        expr: Box::new(Expr::Binary {
20073                            lhs: Box::new(base),
20074                            op: BinOp::JsonGetText,
20075                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20076                        }),
20077                        target: ty,
20078                    },
20079                    alias: Some(col),
20080                }
20081            })
20082            .collect();
20083        let from = if is_set {
20084            let elem_fn = if fn_name.starts_with("jsonb") {
20085                "jsonb_array_elements"
20086            } else {
20087                "json_array_elements"
20088            };
20089            Some(FromClause {
20090                primary: TableRef {
20091                    name: "value".to_string(),
20092                    alias: None,
20093                    only: false,
20094                    as_of_segment: None,
20095                    unnest_expr: Some(Box::new(Expr::FunctionCall {
20096                        name: elem_fn.to_string(),
20097                        args: alloc::vec![arg],
20098                    })),
20099                    unnest_column_aliases: alloc::vec!["value".to_string()],
20100                    with_ordinality: false,
20101                    generate_series_args: None,
20102                    lateral_subquery: None,
20103                    jsonb_each_text_arg: None,
20104                    table_fn_call: None,
20105                    rows_from: None,
20106                    json_table: None,
20107                    scalar_fn_item: false,
20108                },
20109                joins: Vec::new(),
20110            })
20111        } else {
20112            None
20113        };
20114        let inner = SelectStatement {
20115            locking: None,
20116            ctes: Vec::new(),
20117            distinct: false,
20118            distinct_on: Vec::new(),
20119            items,
20120            from,
20121            where_: None,
20122            group_by: None,
20123            group_by_all: false,
20124            having: None,
20125            unions: Vec::new(),
20126            order_by: Vec::new(),
20127            limit: None,
20128            offset: None,
20129            limit_with_ties: false,
20130            window_check_exprs: Vec::new(),
20131        };
20132        Ok(TableRef {
20133            name: alias.clone(),
20134            alias: Some(alias),
20135            only: false,
20136            as_of_segment: None,
20137            unnest_expr: None,
20138            unnest_column_aliases: Vec::new(),
20139            with_ordinality: false,
20140            generate_series_args: None,
20141            lateral_subquery: Some(Box::new(inner)),
20142            jsonb_each_text_arg: None,
20143            table_fn_call: None,
20144            rows_from: None,
20145            json_table: None,
20146            scalar_fn_item: false,
20147        })
20148    }
20149
20150    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20151    /// Returns true when the clause was present. `WITH` alone (a
20152    /// CTE can never start here) is not enough — the ORDINALITY
20153    /// ident must follow, so a stray WITH still errors downstream.
20154    fn absorb_with_ordinality(&mut self) -> bool {
20155        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20156            && matches!(self.tokens.get(self.pos + 1),
20157                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20158        {
20159            self.advance();
20160            self.advance();
20161            true
20162        } else {
20163            false
20164        }
20165    }
20166
20167    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20168    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20169    /// Out-of-line: the caller sits on the FROM recursion chain.
20170    #[inline(never)]
20171    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20172        let fn_name = match self.advance() {
20173            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20174            _ => unreachable!("caller peeked an ident"),
20175        };
20176        self.advance(); // (
20177        let mut args: Vec<Expr> = Vec::new();
20178        if !matches!(self.peek(), Token::RParen) {
20179            loop {
20180                args.push(self.parse_expr(0)?);
20181                if matches!(self.peek(), Token::Comma) {
20182                    self.advance();
20183                    continue;
20184                }
20185                break;
20186            }
20187        }
20188        if !matches!(self.peek(), Token::RParen) {
20189            return Err(self.err(alloc::format!(
20190                "expected ')' after {fn_name}() arguments, got {:?}",
20191                self.peek()
20192            )));
20193        }
20194        self.advance();
20195        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20196        // counter column rides after the function's own, and the alias list
20197        // names it.
20198        let with_ordinality = self.absorb_with_ordinality();
20199        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20200        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20201        Ok(TableRef {
20202            name,
20203            alias: alias_ident,
20204            only: false,
20205            as_of_segment: None,
20206            unnest_expr: None,
20207            unnest_column_aliases,
20208            with_ordinality,
20209            generate_series_args: None,
20210            lateral_subquery: None,
20211            jsonb_each_text_arg: None,
20212            table_fn_call: Some(Box::new((fn_name, args))),
20213            rows_from: None,
20214            json_table: None,
20215            scalar_fn_item: false,
20216        })
20217    }
20218
20219    /// v7.39 (round 205, JSON_TABLE) — parse
20220    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20221    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20222    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20223    #[inline(never)]
20224    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20225        self.advance(); // json_table
20226        self.advance(); // (
20227        let doc = Box::new(self.parse_expr(0)?);
20228        self.expect_comma_json_table()?;
20229        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20230        // Optional `PASSING <expr> AS <name> [, …]`.
20231        let mut passing: Vec<(String, Expr)> = Vec::new();
20232        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20233            self.advance();
20234            loop {
20235                let e = self.parse_expr(0)?;
20236                if !matches!(self.peek(), Token::As) {
20237                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20238                }
20239                self.advance();
20240                let vname = match self.advance() {
20241                    Token::Ident(s) | Token::QuotedIdent(s) => s,
20242                    other => {
20243                        return Err(self.err(alloc::format!(
20244                            "expected PASSING variable name, got {other:?}"
20245                        )));
20246                    }
20247                };
20248                passing.push((vname, e));
20249                if matches!(self.peek(), Token::Comma) {
20250                    self.advance();
20251                    continue;
20252                }
20253                break;
20254            }
20255        }
20256        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20257            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20258        }
20259        self.advance();
20260        let columns = self.parse_json_table_columns()?;
20261        if !matches!(self.peek(), Token::RParen) {
20262            return Err(self.err(alloc::format!(
20263                "expected ')' to close JSON_TABLE, got {:?}",
20264                self.peek()
20265            )));
20266        }
20267        self.advance();
20268        let alias_ident = self.parse_optional_alias()?;
20269        let name = alias_ident
20270            .clone()
20271            .unwrap_or_else(|| String::from("json_table"));
20272        Ok(TableRef {
20273            name,
20274            alias: alias_ident,
20275            only: false,
20276            as_of_segment: None,
20277            unnest_expr: None,
20278            unnest_column_aliases: Vec::new(),
20279            with_ordinality: false,
20280            generate_series_args: None,
20281            lateral_subquery: None,
20282            jsonb_each_text_arg: None,
20283            table_fn_call: None,
20284            rows_from: None,
20285            json_table: Some(Box::new(crate::ast::JsonTable {
20286                doc,
20287                row_path,
20288                columns,
20289                passing,
20290            })),
20291            scalar_fn_item: false,
20292        })
20293    }
20294
20295    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20296        if !matches!(self.peek(), Token::Comma) {
20297            return Err(self.err(alloc::format!(
20298                "expected ',' after JSON_TABLE document, got {:?}",
20299                self.peek()
20300            )));
20301        }
20302        self.advance();
20303        Ok(())
20304    }
20305
20306    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20307        match self.advance() {
20308            Token::String(s) => Ok(s),
20309            other => Err(self.err(alloc::format!(
20310                "expected {what} string literal, got {other:?}"
20311            ))),
20312        }
20313    }
20314
20315    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20316    #[inline(never)]
20317    fn parse_json_table_columns(
20318        &mut self,
20319    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20320        if !matches!(self.peek(), Token::LParen) {
20321            return Err(self.err("expected '(' after COLUMNS".into()));
20322        }
20323        self.advance();
20324        let mut cols = Vec::new();
20325        loop {
20326            cols.push(self.parse_json_table_one_column()?);
20327            if matches!(self.peek(), Token::Comma) {
20328                self.advance();
20329                continue;
20330            }
20331            break;
20332        }
20333        if !matches!(self.peek(), Token::RParen) {
20334            return Err(self.err(alloc::format!(
20335                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20336                self.peek()
20337            )));
20338        }
20339        self.advance();
20340        Ok(cols)
20341    }
20342
20343    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20344        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20345        // NESTED [PATH] '<p>' COLUMNS (...)
20346        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20347            self.advance();
20348            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20349                self.advance();
20350            }
20351            let path = self.parse_json_string_literal("NESTED PATH")?;
20352            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20353                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20354            }
20355            self.advance();
20356            let columns = self.parse_json_table_columns()?;
20357            return Ok(JsonTableColumn::Nested { path, columns });
20358        }
20359        // <name> ...
20360        let name = match self.advance() {
20361            Token::Ident(s) | Token::QuotedIdent(s) => s,
20362            other => {
20363                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20364            }
20365        };
20366        // <name> FOR ORDINALITY
20367        if matches!(self.peek(), Token::For) {
20368            self.advance();
20369            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20370                return Err(self.err("expected ORDINALITY after FOR".into()));
20371            }
20372            self.advance();
20373            return Ok(JsonTableColumn::Ordinality { name });
20374        }
20375        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20376        let ty = self.parse_column_type_name()?;
20377        let mut format_json = false;
20378        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20379            self.advance();
20380            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20381                return Err(self.err("expected JSON after FORMAT".into()));
20382            }
20383            self.advance();
20384            format_json = true;
20385        }
20386        let mut exists = false;
20387        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20388            self.advance();
20389            exists = true;
20390        }
20391        let mut path = alloc::format!("$.{name}");
20392        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20393            self.advance();
20394            path = self.parse_json_string_literal("column PATH")?;
20395        }
20396        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20397            // `FORMAT JSON` after PATH (alternate placement).
20398            self.advance();
20399            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20400                self.advance();
20401            }
20402            format_json = true;
20403        }
20404        let mut wrapper = false;
20405        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20406            self.advance();
20407            // optional CONDITIONAL/UNCONDITIONAL
20408            if matches!(self.peek(), Token::Ident(s)
20409                if s.eq_ignore_ascii_case("unconditional")
20410                    || s.eq_ignore_ascii_case("conditional"))
20411            {
20412                self.advance();
20413            }
20414            if !matches!(self.peek(), Token::Ident(s)
20415                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20416            {
20417                return Err(self.err("expected WRAPPER after WITH".into()));
20418            }
20419            self.advance();
20420            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20421            if matches!(self.peek(), Token::Ident(s)
20422                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20423            {
20424                self.advance();
20425            }
20426            wrapper = true;
20427        }
20428        // ON EMPTY / ON ERROR clauses (two, in any order).
20429        let mut on_empty = JsonTableOnBehavior::Null;
20430        let mut on_error = JsonTableOnBehavior::Null;
20431        for _ in 0..2 {
20432            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20433            {
20434                self.advance();
20435                Some(JsonTableOnBehavior::Error)
20436            } else if matches!(self.peek(), Token::Null) {
20437                self.advance();
20438                Some(JsonTableOnBehavior::Null)
20439            } else if matches!(self.peek(), Token::Default) {
20440                self.advance();
20441                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20442            } else {
20443                None
20444            };
20445            let Some(behavior) = behavior else { break };
20446            // `ON {EMPTY|ERROR}`
20447            if !matches!(self.peek(), Token::On) {
20448                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20449            }
20450            self.advance();
20451            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20452                self.advance();
20453                on_empty = behavior;
20454            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20455                self.advance();
20456                on_error = behavior;
20457            } else {
20458                return Err(self.err("expected EMPTY or ERROR after ON".into()));
20459            }
20460        }
20461        Ok(JsonTableColumn::Regular {
20462            name,
20463            ty,
20464            path,
20465            exists,
20466            format_json,
20467            wrapper,
20468            on_empty,
20469            on_error,
20470        })
20471    }
20472
20473    fn parse_optional_alias_with_columns(
20474        &mut self,
20475    ) -> Result<(Option<String>, Vec<String>), ParseError> {
20476        let alias = self.parse_optional_alias()?;
20477        if alias.is_none() {
20478            return Ok((None, Vec::new()));
20479        }
20480        let mut cols: Vec<String> = Vec::new();
20481        if matches!(self.peek(), Token::LParen) {
20482            self.advance();
20483            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20484                self.advance();
20485                cols.push(s);
20486                if matches!(self.peek(), Token::Comma) {
20487                    self.advance();
20488                    continue;
20489                }
20490                break;
20491            }
20492            if matches!(self.peek(), Token::RParen) {
20493                self.advance();
20494            }
20495        }
20496        Ok((alias, cols))
20497    }
20498
20499    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20500    /// whose keyword token was already consumed and whose `(` is the
20501    /// current token. Factored out of `parse_atom` (and marked
20502    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20503    /// recursive `parse_atom` frame — inlining them there enlarges the
20504    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20505    /// against, risking an overflow before the budget triggers.
20506    #[inline(never)]
20507    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20508        self.advance(); // (
20509        let mut args = Vec::new();
20510        if !matches!(self.peek(), Token::RParen) {
20511            loop {
20512                args.push(self.parse_expr(0)?);
20513                match self.peek() {
20514                    Token::Comma => {
20515                        self.advance();
20516                    }
20517                    Token::RParen => break,
20518                    other => {
20519                        return Err(self.err(alloc::format!(
20520                            "expected ',' or ')' in {name}() args, got {other:?}"
20521                        )));
20522                    }
20523                }
20524            }
20525        }
20526        self.advance(); // )
20527        Ok(Expr::FunctionCall {
20528            name: name.into(),
20529            args,
20530        })
20531    }
20532
20533    /// FROM-clause: a primary table reference plus zero-or-more joined
20534    /// peers expressed via either `, <table>` (cross-product, no ON) or
20535    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20536    /// v1.10 keeps the join list flat (left-associative nested-loop
20537    /// semantics).
20538    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20539        let primary = self.parse_table_ref()?;
20540        let primary_qual = primary
20541            .alias
20542            .clone()
20543            .unwrap_or_else(|| primary.name.clone());
20544        let joins = self.parse_from_joins(&primary_qual)?;
20545        Ok(FromClause { primary, joins })
20546    }
20547
20548    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20549    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20550    /// SAME grammar after its target table has already been consumed.
20551    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20552    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20553    /// be parsed forward, once.)
20554    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20555    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20556    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20557    /// desugaring, which needs a name for the left side of each equality.
20558    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20559        let mut joins = Vec::new();
20560        loop {
20561            // `, <table>` — cross-product with no ON.
20562            if matches!(self.peek(), Token::Comma) {
20563                self.advance();
20564                let table = self.parse_table_ref()?;
20565                joins.push(FromJoin {
20566                    kind: JoinKind::Cross,
20567                    table,
20568                    on: None,
20569                    using_cols: None,
20570                    natural: false,
20571                });
20572                continue;
20573            }
20574            // v7.37.16 — optional leading `NATURAL` before the join
20575            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20576            // not a lexer keyword (it arrives as a bare Ident), so match
20577            // it case-insensitively here. When present, no ON/USING
20578            // clause is allowed — the common columns are resolved at
20579            // execution time.
20580            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20581            if natural {
20582                self.advance();
20583            }
20584            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20585            // CROSS JOIN, and bare JOIN (defaults to INNER).
20586            let kind =
20587                match self.peek() {
20588                    Token::Inner => {
20589                        self.advance();
20590                        if !matches!(self.peek(), Token::Join) {
20591                            return Err(self
20592                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20593                        }
20594                        self.advance();
20595                        JoinKind::Inner
20596                    }
20597                    Token::Left => {
20598                        self.advance();
20599                        if matches!(self.peek(), Token::Outer) {
20600                            self.advance();
20601                        }
20602                        if !matches!(self.peek(), Token::Join) {
20603                            return Err(self.err(format!(
20604                                "expected JOIN after LEFT [OUTER], got {:?}",
20605                                self.peek()
20606                            )));
20607                        }
20608                        self.advance();
20609                        JoinKind::Left
20610                    }
20611                    Token::Cross => {
20612                        self.advance();
20613                        if !matches!(self.peek(), Token::Join) {
20614                            return Err(self
20615                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20616                        }
20617                        self.advance();
20618                        JoinKind::Cross
20619                    }
20620                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20621                    Token::Right => {
20622                        self.advance();
20623                        if matches!(self.peek(), Token::Outer) {
20624                            self.advance();
20625                        }
20626                        if !matches!(self.peek(), Token::Join) {
20627                            return Err(self.err(format!(
20628                                "expected JOIN after RIGHT [OUTER], got {:?}",
20629                                self.peek()
20630                            )));
20631                        }
20632                        self.advance();
20633                        JoinKind::Right
20634                    }
20635                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20636                    Token::Full => {
20637                        self.advance();
20638                        if matches!(self.peek(), Token::Outer) {
20639                            self.advance();
20640                        }
20641                        if !matches!(self.peek(), Token::Join) {
20642                            return Err(self.err(format!(
20643                                "expected JOIN after FULL [OUTER], got {:?}",
20644                                self.peek()
20645                            )));
20646                        }
20647                        self.advance();
20648                        JoinKind::FullOuter
20649                    }
20650                    Token::Join => {
20651                        self.advance();
20652                        JoinKind::Inner
20653                    }
20654                    _ => break,
20655                };
20656            let table = self.parse_table_ref()?;
20657            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20658            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20659            // where prev_table is the most-recent left-side table
20660            // (the previous join's table if any, else the FROM primary).
20661            // PG semantics around column merging are richer (USING'd
20662            // cols become deduplicated single output columns); for
20663            // sugar purposes the predicate-only form covers the
20664            // baseline corpus shape and chained `… JOIN x USING (k)
20665            // JOIN y USING (k)` calls.
20666            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20667            // common columns resolve at execution time.
20668            if natural {
20669                joins.push(FromJoin {
20670                    kind,
20671                    table,
20672                    on: None,
20673                    using_cols: None,
20674                    natural: true,
20675                });
20676                continue;
20677            }
20678            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20679            // v7.37.16 — capture the USING column list (in addition to
20680            // the ON desugar below) so the executor can perform PG's
20681            // column-merge on the output side.
20682            let mut using_cols: Option<Vec<String>> = None;
20683            let on = if matches!(self.peek(), Token::On) {
20684                self.advance();
20685                Some(self.parse_expr(0)?)
20686            } else if using_match {
20687                self.advance();
20688                if !matches!(self.peek(), Token::LParen) {
20689                    return Err(
20690                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20691                    );
20692                }
20693                self.advance();
20694                let mut cols: Vec<String> = Vec::new();
20695                loop {
20696                    match self.peek().clone() {
20697                        Token::Ident(s) | Token::QuotedIdent(s) => {
20698                            self.advance();
20699                            cols.push(s);
20700                        }
20701                        other => {
20702                            return Err(self.err(format!(
20703                                "expected column name inside USING (…), got {other:?}"
20704                            )));
20705                        }
20706                    }
20707                    match self.peek() {
20708                        Token::Comma => {
20709                            self.advance();
20710                            continue;
20711                        }
20712                        Token::RParen => {
20713                            self.advance();
20714                            break;
20715                        }
20716                        other => {
20717                            return Err(self.err(format!(
20718                                "expected ',' or ')' inside USING (…), got {other:?}"
20719                            )));
20720                        }
20721                    }
20722                }
20723                if cols.is_empty() {
20724                    return Err(self.err("USING (…) requires at least one column".to_string()));
20725                }
20726                using_cols = Some(cols.clone());
20727                // Pick the left-side alias: prev join's table if any,
20728                // else FROM primary. Use alias when present, else
20729                // table name (PG-equivalent qualifier).
20730                let left_qual: String = joins
20731                    .last()
20732                    .map(|j| {
20733                        j.table
20734                            .alias
20735                            .clone()
20736                            .unwrap_or_else(|| j.table.name.clone())
20737                    })
20738                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20739                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20740                let mut iter = cols.into_iter().map(|c| Expr::Binary {
20741                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20742                        qualifier: Some(left_qual.clone()),
20743                        name: c.clone(),
20744                    })),
20745                    op: crate::ast::BinOp::Eq,
20746                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20747                        qualifier: Some(right_qual.clone()),
20748                        name: c,
20749                    })),
20750                });
20751                let first = iter.next().expect("at least one col");
20752                Some(iter.fold(first, |acc, pred| Expr::Binary {
20753                    lhs: alloc::boxed::Box::new(acc),
20754                    op: crate::ast::BinOp::And,
20755                    rhs: alloc::boxed::Box::new(pred),
20756                }))
20757            } else if kind == JoinKind::Cross {
20758                None
20759            } else {
20760                return Err(self.err(format!(
20761                    "expected ON or USING after {:?} JOIN, got {:?}",
20762                    kind,
20763                    self.peek()
20764                )));
20765            };
20766            joins.push(FromJoin {
20767                kind,
20768                table,
20769                on,
20770                using_cols,
20771                natural: false,
20772            });
20773        }
20774        Ok(joins)
20775    }
20776
20777    /// Optional alias after an expression or table:
20778    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20779    /// accepted (PG-style implicit alias). Returns `None` if the next token
20780    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20781    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20782        if matches!(self.peek(), Token::As) {
20783            self.advance();
20784            // v7.39 (round 340, V56) — after AS the next token MUST be an
20785            // identifier. This used to return None and "let the caller
20786            // surface the error on the next expectation", but when AS is
20787            // the LAST token there is no next expectation: `SELECT 1 AS`
20788            // parsed clean and silently dropped the alias. PG rejects it.
20789            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20790                return self.expect_ident_like().map(Some);
20791            }
20792            return Err(self.err(alloc::format!(
20793                "expected an alias after AS, got {:?}",
20794                self.peek()
20795            )));
20796        }
20797        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
20798        // grammar reserves a long list of follow-keywords from the
20799        // alias slot. SPG's bareword approximation: skip a small
20800        // set of idents that would otherwise be swallowed as the
20801        // table alias and break trailing clauses like CREATE
20802        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
20803        // CONFLICT WHERE shapes.
20804        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
20805            if is_alias_stopword(s) {
20806                return Ok(None);
20807            }
20808            return Ok(self.expect_ident_like().ok());
20809        }
20810        Ok(None)
20811    }
20812
20813    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
20814    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
20815        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
20816        // error beats a stack overflow (an overflow aborts the
20817        // embedding host process).
20818        self.enter_nested()?;
20819        let r = self.parse_expr_inner(min_prec);
20820        self.nest_depth -= 1;
20821        r
20822    }
20823
20824    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
20825    /// When the upcoming tokens form one, return the underlying
20826    /// operator token and the position just past the closing paren
20827    /// so the binary loop can dispatch on the plain operator.
20828    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
20829        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
20830            return None;
20831        }
20832        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
20833            return None;
20834        }
20835        let mut i = self.pos + 2;
20836        // Optional schema qualifier (pg_catalog.<op> etc.).
20837        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
20838            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
20839        {
20840            i += 2;
20841        }
20842        let op_tok = self.tokens.get(i)?.clone();
20843        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
20844            return None;
20845        }
20846        Some((i + 2, op_tok))
20847    }
20848
20849    /// PG operator symbols that lower onto function calls in
20850    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
20851    /// family → regexp_like, comparison rung), `^@` (starts_with,
20852    /// comparison rung), `^` (power, tighter than `*`), `#`
20853    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
20854    /// subset of the OR bits so the subtraction never borrows).
20855    fn try_symbol_operator(
20856        &mut self,
20857        lhs: &Expr,
20858        min_prec: u8,
20859    ) -> Result<Option<Expr>, ParseError> {
20860        enum Sym {
20861            Regex { ci: bool, negated: bool },
20862            Like { ci: bool, negated: bool },
20863            StartsWith,
20864            Power,
20865            Xor,
20866            RangeAdjacent,
20867        }
20868        // v7.39 (IS-precedence knife) — the low-precedence postfix
20869        // predicates ride this existing leaf call (zero new frame slots
20870        // on the nesting chain).
20871        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
20872            return Ok(Some(e));
20873        }
20874        let (sym, prec): (Sym, u8) = match self.peek() {
20875            Token::Tilde => (
20876                Sym::Regex {
20877                    ci: false,
20878                    negated: false,
20879                },
20880                5,
20881            ),
20882            Token::TildeStar => (
20883                Sym::Regex {
20884                    ci: true,
20885                    negated: false,
20886                },
20887                5,
20888            ),
20889            Token::NotTilde => (
20890                Sym::Regex {
20891                    ci: false,
20892                    negated: true,
20893                },
20894                5,
20895            ),
20896            Token::NotTildeStar => (
20897                Sym::Regex {
20898                    ci: true,
20899                    negated: true,
20900                },
20901                5,
20902            ),
20903            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
20904            Token::DoubleTilde => (
20905                Sym::Like {
20906                    ci: false,
20907                    negated: false,
20908                },
20909                5,
20910            ),
20911            Token::DoubleTildeStar => (
20912                Sym::Like {
20913                    ci: true,
20914                    negated: false,
20915                },
20916                5,
20917            ),
20918            Token::NotDoubleTilde => (
20919                Sym::Like {
20920                    ci: false,
20921                    negated: true,
20922                },
20923                5,
20924            ),
20925            Token::NotDoubleTildeStar => (
20926                Sym::Like {
20927                    ci: true,
20928                    negated: true,
20929                },
20930                5,
20931            ),
20932            Token::CaretAt => (Sym::StartsWith, 5),
20933            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
20934            // tighter than `* / & |`, which the prec-9 rung preserves —
20935            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
20936            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
20937            Token::Caret => (Sym::Power, 9),
20938            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
20939            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
20940            Token::Hash => (Sym::Xor, 6),
20941            Token::Adjacent => (Sym::RangeAdjacent, 5),
20942            _ => return Ok(None),
20943        };
20944        if prec < min_prec {
20945            return Ok(None);
20946        }
20947        self.advance();
20948        let rhs = self.parse_expr(prec + 1)?;
20949        let out = match sym {
20950            Sym::Regex { ci, negated } => {
20951                let mut args = alloc::vec![lhs.clone(), rhs];
20952                if ci {
20953                    args.push(Expr::Literal(Literal::String(String::from("i"))));
20954                }
20955                maybe_not(
20956                    Expr::FunctionCall {
20957                        name: String::from("regexp_like"),
20958                        args,
20959                    },
20960                    negated,
20961                )
20962            }
20963            Sym::Like { ci, negated } => Expr::Like {
20964                expr: alloc::boxed::Box::new(lhs.clone()),
20965                pattern: alloc::boxed::Box::new(rhs),
20966                negated,
20967                case_insensitive: ci,
20968            },
20969            Sym::StartsWith => Expr::FunctionCall {
20970                name: String::from("starts_with"),
20971                args: alloc::vec![lhs.clone(), rhs],
20972            },
20973            Sym::Power => Expr::FunctionCall {
20974                name: String::from("power"),
20975                args: alloc::vec![lhs.clone(), rhs],
20976            },
20977            // `#` bitwise XOR — a real operator now (was desugared to
20978            // `(a|b)-(a&b)`, algebraically identical for integers but
20979            // undefined for bit strings; the direct op handles both).
20980            Sym::Xor => Expr::Binary {
20981                lhs: Box::new(lhs.clone()),
20982                op: BinOp::BitXor,
20983                rhs: Box::new(rhs),
20984            },
20985            // range `-|-` "is adjacent to" — lowered to a catalog function.
20986            Sym::RangeAdjacent => Expr::FunctionCall {
20987                name: String::from("range_adjacent"),
20988                args: alloc::vec![lhs.clone(), rhs],
20989            },
20990        };
20991        Ok(Some(out))
20992    }
20993
20994    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
20995    /// predicates, moved out of the tight postfix-cast loop: PG binds
20996    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
20997    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
20998    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
20999    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21000    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21001    /// when nothing at this position belongs to the family. Out-of-line
21002    /// (`inline(never)`): the caller sits on the per-nesting-level frame
21003    /// chain that MAX_NEST_DEPTH is tuned against.
21004    #[inline(never)]
21005    fn parse_postfix_predicate(
21006        &mut self,
21007        lhs: &Expr,
21008        min_prec: u8,
21009    ) -> Result<Option<Expr>, ParseError> {
21010        // Reached through try_symbol_operator (an existing leaf call of
21011        // the binary loop) so NO new stack slots land on the per-nesting
21012        // frame chain; the lhs clones only when a predicate actually
21013        // consumes it.
21014        match self.peek() {
21015            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21016            // comparison family rung 5 (each +1 from the pre-XOR ladder).
21017            Token::Is if min_prec <= 4 => {}
21018            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21019            Token::Not
21020                if min_prec <= 5
21021                    && matches!(
21022                        self.tokens.get(self.pos + 1),
21023                        Some(Token::Between | Token::In | Token::Like)
21024                    ) => {}
21025            Token::Not | Token::Ident(_)
21026                if min_prec <= 5
21027                    && (matches!(self.peek(), Token::Ident(s)
21028                            if s.eq_ignore_ascii_case("ilike")
21029                                || (self.mysql_dialect
21030                                    && (s.eq_ignore_ascii_case("regexp")
21031                                        || s.eq_ignore_ascii_case("rlike")))
21032                                || (s.eq_ignore_ascii_case("similar")
21033                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21034                        || (matches!(self.peek(), Token::Not)
21035                            && matches!(self.tokens.get(self.pos + 1),
21036                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21037                                    || (self.mysql_dialect
21038                                        && (s.eq_ignore_ascii_case("regexp")
21039                                            || s.eq_ignore_ascii_case("rlike")))
21040                                    || s.eq_ignore_ascii_case("similar")))) => {}
21041            _ => return Ok(None),
21042        }
21043        let mut expr = lhs.clone();
21044        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21045        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21046        if min_prec <= 4 {
21047            if matches!(self.peek(), Token::Is) {
21048                self.advance();
21049                let negated = if matches!(self.peek(), Token::Not) {
21050                    self.advance();
21051                    true
21052                } else {
21053                    false
21054                };
21055                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21056                // mailrs pg_dump.
21057                if matches!(self.peek(), Token::Distinct) {
21058                    self.advance();
21059                    if !matches!(self.peek(), Token::From) {
21060                        return Err(self.err(format!(
21061                            "expected FROM after IS{} DISTINCT, got {:?}",
21062                            if negated { " NOT" } else { "" },
21063                            self.peek()
21064                        )));
21065                    }
21066                    self.advance();
21067                    // Right-hand side: parse at the same precedence
21068                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21069                    // groups as `x IS DISTINCT FROM (a + b)`.
21070                    let rhs = self.parse_expr(5)?;
21071                    let op = if negated {
21072                        BinOp::IsNotDistinctFrom
21073                    } else {
21074                        BinOp::IsDistinctFrom
21075                    };
21076                    expr = Expr::Binary {
21077                        op,
21078                        lhs: Box::new(expr),
21079                        rhs: Box::new(rhs),
21080                    };
21081                    {
21082                        return Ok(Some(expr));
21083                    }
21084                }
21085                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21086                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21087                // Lowers onto pg_is_json(x, kind); NOT wraps the
21088                // call in a logical negation.
21089                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21090                if s.eq_ignore_ascii_case("json"))
21091                {
21092                    self.advance(); // JSON
21093                    let kind = match self.peek() {
21094                        Token::Ident(s) | Token::QuotedIdent(s)
21095                            if matches!(
21096                                s.to_ascii_lowercase().as_str(),
21097                                "value" | "object" | "array" | "scalar"
21098                            ) =>
21099                        {
21100                            let k = s.to_ascii_lowercase();
21101                            self.advance();
21102                            k
21103                        }
21104                        _ => "value".to_string(),
21105                    };
21106                    let call = Expr::FunctionCall {
21107                        name: "pg_is_json".to_string(),
21108                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21109                    };
21110                    expr = if negated {
21111                        Expr::Unary {
21112                            op: UnOp::Not,
21113                            expr: Box::new(call),
21114                        }
21115                    } else {
21116                        call
21117                    };
21118                    {
21119                        return Ok(Some(expr));
21120                    }
21121                }
21122                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21123                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21124                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21125                {
21126                    let form_kw = match self.peek() {
21127                        Token::Ident(s) | Token::QuotedIdent(s)
21128                            if matches!(
21129                                s.to_ascii_uppercase().as_str(),
21130                                "NFC" | "NFD" | "NFKC" | "NFKD"
21131                            ) && matches!(
21132                                self.tokens.get(self.pos + 1),
21133                                Some(Token::Ident(n) | Token::QuotedIdent(n))
21134                                    if n.eq_ignore_ascii_case("normalized")
21135                            ) =>
21136                        {
21137                            Some(s.to_ascii_uppercase())
21138                        }
21139                        _ => None,
21140                    };
21141                    let bare_normalized = form_kw.is_none()
21142                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21143                        if s.eq_ignore_ascii_case("normalized"));
21144                    if form_kw.is_some() || bare_normalized {
21145                        if form_kw.is_some() {
21146                            self.advance(); // form keyword
21147                        }
21148                        self.advance(); // NORMALIZED
21149                        let mut args = alloc::vec![expr];
21150                        if let Some(f) = form_kw {
21151                            args.push(Expr::Literal(Literal::String(f)));
21152                        }
21153                        let call = Expr::FunctionCall {
21154                            name: "is_normalized".to_string(),
21155                            args,
21156                        };
21157                        expr = if negated {
21158                            Expr::Unary {
21159                                op: UnOp::Not,
21160                                expr: Box::new(call),
21161                            }
21162                        } else {
21163                            call
21164                        };
21165                        {
21166                            return Ok(Some(expr));
21167                        }
21168                    }
21169                }
21170                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21171                // three-valued boolean tests. IS TRUE/FALSE never
21172                // return NULL, so they lower to CASE forms whose
21173                // ELSE catches the NULL branch; IS UNKNOWN on a
21174                // boolean is exactly IS NULL.
21175                if matches!(self.peek(), Token::True | Token::False)
21176                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21177                {
21178                    let tok = self.advance();
21179                    let test = match tok {
21180                        Token::True => Some(true),
21181                        Token::False => Some(false),
21182                        _ => None, // UNKNOWN
21183                    };
21184                    // v7.39 (round 328, V45) — kept as what the user
21185                    // wrote. These used to be lowered here into `CASE` /
21186                    // `IS NULL`; the semantics were right but the AST no
21187                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21188                    // was echoed back as
21189                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21190                    expr = Expr::BoolTest {
21191                        expr: Box::new(expr),
21192                        value: test,
21193                        negated,
21194                    };
21195                    {
21196                        return Ok(Some(expr));
21197                    }
21198                }
21199                if !matches!(self.peek(), Token::Null) {
21200                    return Err(self.err(format!(
21201                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21202                    if negated { " NOT" } else { "" },
21203                    self.peek()
21204                )));
21205                }
21206                self.advance();
21207                expr = Expr::IsNull {
21208                    expr: Box::new(expr),
21209                    negated,
21210                };
21211                {
21212                    return Ok(Some(expr));
21213                }
21214            }
21215        }
21216        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21217        if min_prec <= 5 {
21218            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21219            // Look one token ahead so a stray `NOT` not followed by any of
21220            // these flows through to the early return below untouched.
21221            let negated = if matches!(self.peek(), Token::Not) {
21222                let next = self.tokens.get(self.pos + 1);
21223                matches!(next, Some(Token::Between | Token::In | Token::Like))
21224                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21225                    || (self.mysql_dialect
21226                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21227                    || s.eq_ignore_ascii_case("similar"))
21228            } else {
21229                false
21230            };
21231            if negated {
21232                self.advance();
21233            }
21234            if matches!(self.peek(), Token::Between) {
21235                expr = self.parse_between_tail(expr, negated)?;
21236                {
21237                    return Ok(Some(expr));
21238                }
21239            }
21240            if matches!(self.peek(), Token::In) {
21241                if self.suppress_in_tail && !negated {
21242                    // POSITION(sub IN str) — IN belongs to the
21243                    // enclosing function syntax; stop here.
21244                    {
21245                        return Ok(None);
21246                    }
21247                }
21248                expr = self.parse_in_tail(expr, negated)?;
21249                {
21250                    return Ok(Some(expr));
21251                }
21252            }
21253            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21254            // lowers onto the internal __similar_to(expr, pat[, esc]) call
21255            // (the SQL→regex transform runs inside, in the backtracking-
21256            // friendly shape SPG's matcher needs).
21257            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21258                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21259            {
21260                self.advance(); // SIMILAR
21261                self.advance(); // TO
21262                let pattern = self.parse_expr(6)?;
21263                let mut args = alloc::vec![expr, pattern];
21264                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21265                    self.advance();
21266                    args.push(self.parse_expr(6)?);
21267                }
21268                let call = Expr::FunctionCall {
21269                    name: "__similar_to".to_string(),
21270                    args,
21271                };
21272                expr = maybe_not(call, negated);
21273                {
21274                    return Ok(Some(expr));
21275                }
21276            }
21277            if matches!(self.peek(), Token::Like) {
21278                self.advance();
21279                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21280                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21281                    expr = q;
21282                    {
21283                        return Ok(Some(expr));
21284                    }
21285                }
21286                // Pattern at the same precedence as other comparison RHSes —
21287                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21288                let mut pattern = self.parse_expr(6)?;
21289                // `ESCAPE 'c'` — rewrite a literal pattern to the
21290                // default backslash escape at parse time. Custom
21291                // escapes on non-literal patterns would need
21292                // matcher support; error honestly.
21293                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21294                    self.advance();
21295                    let esc = self.parse_expr(6)?;
21296                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21297                }
21298                expr = Expr::Like {
21299                    expr: Box::new(expr),
21300                    pattern: Box::new(pattern),
21301                    negated,
21302                    case_insensitive: false,
21303                };
21304                {
21305                    return Ok(Some(expr));
21306                }
21307            }
21308            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21309            // keyword reaches us as a plain identifier.
21310            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21311                self.advance();
21312                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21313                    expr = q;
21314                    {
21315                        return Ok(Some(expr));
21316                    }
21317                }
21318                let pattern = self.parse_expr(6)?;
21319                expr = Expr::Like {
21320                    expr: Box::new(expr),
21321                    pattern: Box::new(pattern),
21322                    negated,
21323                    case_insensitive: true,
21324                };
21325                {
21326                    return Ok(Some(expr));
21327                }
21328            }
21329            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21330            // operator (RLIKE is the alias). It is a keyword, not `~`, and
21331            // matches case-insensitively under the default collation, so it
21332            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21333            // `~*` operator uses, wrapped in NOT when negated.
21334            if self.mysql_dialect
21335                && matches!(self.peek(), Token::Ident(s)
21336                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21337            {
21338                self.advance();
21339                let pattern = self.parse_expr(6)?;
21340                let call = Expr::FunctionCall {
21341                    name: String::from("regexp_like"),
21342                    args: alloc::vec![
21343                        expr,
21344                        pattern,
21345                        Expr::Literal(Literal::String(String::from("i"))),
21346                    ],
21347                };
21348                return Ok(Some(maybe_not(call, negated)));
21349            }
21350        }
21351        let _ = expr;
21352        Ok(None)
21353    }
21354
21355    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21356        let mut lhs = self.parse_unary()?;
21357        let mut chain_len = 0usize;
21358        loop {
21359            // OPERATOR([schema.]op) reduces to its underlying
21360            // operator token before the normal dispatch.
21361            let explicit = self.peek_explicit_operator();
21362            let dispatch = match &explicit {
21363                Some((_, tok)) => self.binop_here(tok),
21364                None => self.binop_here(self.peek()),
21365            };
21366            let Some((op, prec)) = dispatch else {
21367                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21368                // of the symbol family. `binop_here` answers None for them
21369                // because they lower onto function calls rather than a
21370                // BinOp, and the fallback below reads `self.peek()` — the
21371                // word OPERATOR, not the operator. `pg_dump` writes every
21372                // catalog predicate this way, so its first query failed
21373                // and no dump ran:
21374                //
21375                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21376                //
21377                // Collapsing the wrapper to the operator it names puts the
21378                // token where the fallback already looks.
21379                if let Some((next, op_tok)) = explicit {
21380                    self.tokens.splice(self.pos..next, [op_tok]);
21381                }
21382                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21383                    lhs = e;
21384                    chain_len += 1;
21385                    if chain_len > MAX_BINARY_CHAIN {
21386                        return Err(self.err(alloc::format!(
21387                            "more than {MAX_BINARY_CHAIN} chained binary operators"
21388                        )));
21389                    }
21390                    continue;
21391                }
21392                break;
21393            };
21394            if prec < min_prec {
21395                break;
21396            }
21397            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21398            // iteratively but evaluates and drops recursively;
21399            // depth beyond the budget overflows worker stacks.
21400            chain_len += 1;
21401            if chain_len > MAX_BINARY_CHAIN {
21402                return Err(self.err(alloc::format!(
21403                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21404                )));
21405            }
21406            match explicit {
21407                Some((end_pos, _)) => self.pos = end_pos,
21408                None => {
21409                    self.advance();
21410                }
21411            }
21412            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21413            // ANY is a bare ident; ALL is a reserved Token. Both
21414            // require an immediate `(` to disambiguate from
21415            // identifier columns named `any` / `all`.
21416            let any_kind = match self.peek() {
21417                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21418                    Some(false)
21419                }
21420                Token::Ident(s) | Token::QuotedIdent(s)
21421                    if (s.eq_ignore_ascii_case("any")
21422                        || s.eq_ignore_ascii_case("some")
21423                        || s.eq_ignore_ascii_case("all"))
21424                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21425                {
21426                    Some(!s.eq_ignore_ascii_case("all"))
21427                }
21428                _ => None,
21429            };
21430            if let Some(is_any) = any_kind {
21431                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21432                continue;
21433            }
21434            let rhs = self.parse_expr(prec + 1)?;
21435            lhs = Expr::Binary {
21436                lhs: Box::new(lhs),
21437                op,
21438                rhs: Box::new(rhs),
21439            };
21440        }
21441        Ok(lhs)
21442    }
21443
21444    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21445    /// and the array form.
21446    ///
21447    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21448    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21449    /// this block's `Expr` temporaries and four `format!` sites slots in
21450    /// that frame on every level of `((((1))))`, which never reaches it.
21451    #[inline(never)]
21452    fn parse_any_all_rhs(
21453        &mut self,
21454        lhs: Expr,
21455        op: BinOp,
21456        is_any: bool,
21457    ) -> Result<Expr, ParseError> {
21458        self.advance(); // ident
21459        self.advance(); // (
21460        // `x op ANY (SELECT …)` — the quantified-subquery
21461        // form. `= ANY` is exactly IN; the other operators
21462        // lower onto EXISTS over the subquery as a derived
21463        // table, comparing against its single projection
21464        // aliased __v (x's columns resolve correlated).
21465        // ALL is the negated-EXISTS complement; a NULL
21466        // element makes PG return NULL where this lowering
21467        // returns true — the NOT NULL column case (the
21468        // practical one) is exact.
21469        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21470            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21471            // legal PG too (round-151 sibling). Out-of-line
21472            // (#[inline(never)] helper) — this sits on
21473            // parse_expr's recursive frame and the two-armed
21474            // SELECT temporary blew the nesting-budget stack.
21475            let mut sub = self.parse_any_all_select_body()?;
21476            if !matches!(self.peek(), Token::RParen) {
21477                return Err(self.err(alloc::format!(
21478                    "expected ')' after ANY/ALL subquery, got {:?}",
21479                    self.peek()
21480                )));
21481            }
21482            self.advance();
21483            if sub.items.len() != 1 {
21484                return Err(self.err(alloc::format!(
21485                    "ANY/ALL subquery must return one column, got {}",
21486                    sub.items.len()
21487                )));
21488            }
21489            if is_any && matches!(op, BinOp::Eq) {
21490                return Ok(Expr::InSubquery {
21491                    expr: Box::new(lhs),
21492                    subquery: Box::new(sub),
21493                    negated: false,
21494                });
21495            }
21496            // The engine's subquery resolvers materialise
21497            // the single-column result into an ARRAY the
21498            // existing AnyAll three-valued eval consumes.
21499            return Ok(Expr::AnyAll {
21500                expr: Box::new(lhs),
21501                op,
21502                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21503                is_any,
21504            });
21505        }
21506        let arr = self.parse_expr(0)?;
21507        if !matches!(self.peek(), Token::RParen) {
21508            return Err(self.err(alloc::format!(
21509                "expected ')' after ANY/ALL argument, got {:?}",
21510                self.peek()
21511            )));
21512        }
21513        self.advance();
21514        Ok(Expr::AnyAll {
21515            expr: Box::new(lhs),
21516            op,
21517            array: Box::new(arr),
21518            is_any,
21519        })
21520    }
21521
21522    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21523    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21524    #[inline(never)]
21525    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21526        self.advance();
21527        let e = self.parse_expr(9)?;
21528        Ok(build_center_call(e))
21529    }
21530
21531    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21532    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21533    /// unary minus.
21534    ///
21535    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21536    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21537    /// the Expr-sized local stays out of that frame.
21538    #[inline(never)]
21539    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21540        self.advance();
21541        let e = self.parse_expr(9)?;
21542        Ok(Expr::FunctionCall {
21543            name: alloc::string::String::from(name),
21544            args: alloc::vec![e],
21545        })
21546    }
21547
21548    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21549    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21550    #[inline(never)]
21551    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21552        self.advance();
21553        let e = self.parse_expr(9)?;
21554        Ok(Expr::FunctionCall {
21555            name: alloc::string::String::from(if vertical {
21556                "isvertical"
21557            } else {
21558                "ishorizontal"
21559            }),
21560            args: alloc::vec![e],
21561        })
21562    }
21563
21564    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21565    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21566    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21567    #[inline(never)]
21568    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21569        self.advance();
21570        let e = self.parse_expr(9)?;
21571        Ok(Expr::Cast {
21572            expr: Box::new(e),
21573            target: CastTarget::Named("binary".to_string()),
21574        })
21575    }
21576
21577    /// The prefix operators that share one shape: take the token, parse
21578    /// an operand at `prec`, wrap it.
21579    ///
21580    /// `#[inline(never)]`, and one function instead of five arms, for the
21581    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21582    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21583    /// debug build gives EVERY arm's locals a slot in the frame, whichever
21584    /// arm runs. `((((1))))` reaches none of these arms and was carrying
21585    /// five `Expr`-sized locals per level for them anyway.
21586    #[inline(never)]
21587    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21588        self.advance();
21589        let e = self.parse_expr(prec)?;
21590        Ok(Expr::Unary {
21591            op,
21592            expr: Box::new(e),
21593        })
21594    }
21595
21596    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21597    /// and separate from it because of the literal folding below and the
21598    /// `format!` temporaries that folding needs.
21599    #[inline(never)]
21600    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21601        self.advance();
21602        // v7.39 (round 549) — fold the sign into an integer literal that
21603        // only fits once it is negative.
21604        //
21605        // `9223372036854775808` is one past i64::MAX, so the lexer hands
21606        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21607        // folds the sign first, so `-9223372036854775808` is a bigint
21608        // there — and `-9223372036854775808 - 1` raises "bigint out of
21609        // range" where SPG quietly answered -9223372036854775809, a value
21610        // no bigint can hold. The arithmetic itself was already checked;
21611        // only the literal's type was wrong.
21612        if let Token::Numeric(lit) = self.peek()
21613            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21614        {
21615            self.advance();
21616            return Ok(Expr::Literal(Literal::Integer(folded)));
21617        }
21618        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21619        // `<->` slotted into 5 and arithmetic shifted up).
21620        let e = self.parse_expr(9)?;
21621        Ok(Expr::Unary {
21622            op: UnOp::Neg,
21623            expr: Box::new(e),
21624        })
21625    }
21626
21627    /// tsquery `!!` prefix negation, lowered to the catalog function.
21628    /// Binds like unary minus. Out-of-line for the frame reason on
21629    /// `parse_unary_op`.
21630    #[inline(never)]
21631    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21632        self.advance();
21633        let e = self.parse_expr(9)?;
21634        Ok(Expr::FunctionCall {
21635            name: String::from("tsquery_not"),
21636            args: alloc::vec![e],
21637        })
21638    }
21639
21640    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21641        match self.peek() {
21642            // NOT binds tighter than AND / XOR / OR but looser than
21643            // comparisons — its operand takes everything ≥ the comparison
21644            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21645            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21646            // was rung 3, behaviour-identical when 3 was unused; AND now
21647            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21648            Token::Not => self.parse_unary_op(UnOp::Not, 4),
21649            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21650            // The body is out-of-line: `parse_unary` is one of the three
21651            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21652            // inline arm here overflowed the native stack in
21653            // `nesting_budget_errors_cleanly` — the guard test caught it,
21654            // exactly as the eval-side cliff did in rounds 346 and 351.
21655            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21656                self.parse_binary_prefix()
21657            }
21658            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21659            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21660            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21661            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21662            Token::Minus => self.parse_prefix_minus(),
21663            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21664            // worked only because the lexer reads it as one signed literal;
21665            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21666            // PG18 and MariaDB take all of them. Binds like unary minus.
21667            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21668            // Bitwise NOT binds like unary minus.
21669            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21670            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21671            // "center of" operator; desugars to center(x). The whole arm
21672            // is out-of-line: parse_unary sits on the per-nesting-level
21673            // frame chain that MAX_NEST_DEPTH is tuned against, so no
21674            // Expr-sized local may live in this frame.
21675            Token::TsMatch => self.parse_prefix_center(),
21676            // v7.39 (round 508) — the prefix operators that are named
21677            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21678            // is length. Out-of-line for the same nesting-frame reason as
21679            // parse_prefix_center — parse_unary sits on the recursive cycle
21680            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21681            // live in this frame.
21682            Token::At => self.parse_prefix_call("abs"),
21683            Token::Hash => self.parse_prefix_call("npoints"),
21684            Token::AtMinusAt => self.parse_prefix_call("length"),
21685            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21686            // "is horizontal" (lseg / line); desugars to the existing
21687            // isvertical()/ishorizontal() functions. Out-of-line for the
21688            // same nesting-frame reason as parse_prefix_center.
21689            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21690            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21691            Token::DoubleBang => self.parse_prefix_tsquery_not(),
21692            _ => self.parse_atom(),
21693        }
21694    }
21695
21696    /// Parse a parenthesised scalar subquery body after the caller has consumed
21697    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21698    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21699    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21700    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21701    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21702    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21703    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21704    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21705    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21706    /// tips the deep-nesting test into a stack overflow).
21707    #[inline(never)]
21708    fn array_subquery_ahead(&self) -> bool {
21709        if !matches!(self.peek(), Token::LParen) {
21710            return false;
21711        }
21712        matches!(
21713            self.tokens.get(self.pos + 1),
21714            Some(Token::Select | Token::Values)
21715        ) || matches!(
21716            self.tokens.get(self.pos + 1),
21717            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21718        )
21719    }
21720
21721    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21722    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21723    /// locals stay off parse_atom's recursive frame (round 105).
21724    #[inline(never)]
21725    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21726        self.advance(); // consume `[`
21727        let mut items: Vec<Expr> = Vec::new();
21728        if !matches!(self.peek(), Token::RBracket) {
21729            loop {
21730                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21731                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21732                if matches!(self.peek(), Token::LBracket) {
21733                    items.push(self.parse_array_bracket_body()?);
21734                } else {
21735                    items.push(self.parse_expr(0)?);
21736                }
21737                match self.peek() {
21738                    Token::Comma => {
21739                        self.advance();
21740                    }
21741                    Token::RBracket => break,
21742                    other => {
21743                        return Err(self.err(alloc::format!(
21744                            "expected ',' or ']' in ARRAY literal, got {other:?}"
21745                        )));
21746                    }
21747                }
21748            }
21749        }
21750        self.advance(); // consume `]`
21751        Ok(Expr::Array(items))
21752    }
21753
21754    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21755    /// is already consumed; the current token is `(`. Desugars to a scalar
21756    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21757    /// the subquery's single-column rows in order — reusing the existing
21758    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21759    /// keeps the large `Statement` local off parse_atom's recursive frame.
21760    #[inline(never)]
21761    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21762        self.advance(); // consume `(`
21763        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21764            if w.eq_ignore_ascii_case("with"));
21765        let sub = if is_with {
21766            self.advance(); // WITH
21767            self.parse_with_cte_then_select()?
21768        } else {
21769            self.parse_select_stmt()?
21770        };
21771        if !matches!(self.peek(), Token::RParen) {
21772            return Err(self.err(alloc::format!(
21773                "expected ')' to close ARRAY(subquery), got {:?}",
21774                self.peek()
21775            )));
21776        }
21777        self.advance(); // consume `)`
21778        // Reuse the parser to build the array_agg wrapper from the subquery's
21779        // canonical text — avoids hand-constructing the derived-table AST.
21780        let wrapper = alloc::format!(
21781            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21782        );
21783        let stmt = parse_statement(&wrapper)
21784            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21785        let Statement::Select(sel) = stmt else {
21786            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21787        };
21788        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21789    }
21790
21791    #[inline(never)]
21792    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21793        let inner = if is_with {
21794            self.advance(); // WITH
21795            self.parse_with_cte_then_select()?
21796        } else {
21797            self.parse_select_stmt()?
21798        };
21799        match self.advance() {
21800            Token::RParen => {
21801                let Statement::Select(s) = inner else {
21802                    return Err(ParseError {
21803                        message: "scalar subquery body must be a SELECT".into(),
21804                        token_pos: self.consumed_pos(),
21805                    });
21806                };
21807                Ok(Expr::ScalarSubquery(Box::new(s)))
21808            }
21809            other => Err(ParseError {
21810                message: format!("expected ')' after scalar subquery, got {other:?}"),
21811                token_pos: self.consumed_pos(),
21812            }),
21813        }
21814    }
21815
21816    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
21817    /// literals. The lexer splits them into an ident + string; recombine
21818    /// here. Out-of-line and returning `Option` so `parse_atom` — the
21819    /// recursive frame the 768 KiB stack budget is tuned against — pays no
21820    /// frame for the `body` / `bits` strings and their char loops (the
21821    /// round-367 frame cliff, M20).
21822    #[inline(never)]
21823    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
21824        let is_hex = match self.peek() {
21825            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
21826            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
21827            _ => return None,
21828        };
21829        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
21830            return None;
21831        }
21832        self.advance();
21833        let Token::String(body) = self.advance() else {
21834            unreachable!("guarded above");
21835        };
21836        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
21837        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
21838        // (hex pairs, even count required — MariaDB errors on an odd
21839        // count); `b'1010'` packs its bits big-endian, left-padded to a
21840        // byte. Lower both onto the bytea cast.
21841        if self.mysql_dialect {
21842            if is_hex {
21843                if body.len() % 2 == 1 {
21844                    return Some(Err(self.err(alloc::format!(
21845                        "invalid hex string literal X'{body}': odd digit count"
21846                    ))));
21847                }
21848                for c in body.chars() {
21849                    if !c.is_ascii_hexdigit() {
21850                        return Some(Err(
21851                            self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
21852                        ));
21853                    }
21854                }
21855                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
21856            }
21857            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21858                return Some(Err(
21859                    self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
21860                ));
21861            }
21862            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
21863        }
21864        let bits = if is_hex {
21865            let mut out = String::with_capacity(body.len() * 4);
21866            for c in body.chars() {
21867                let Some(d) = c.to_digit(16) else {
21868                    return Some(Err(self.err(alloc::format!(
21869                        "invalid hexadecimal digit {c:?} in X'…' bit string"
21870                    ))));
21871                };
21872                out.push_str(&alloc::format!("{d:04b}"));
21873            }
21874            out
21875        } else {
21876            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21877                return Some(Err(self.err(alloc::format!(
21878                    "invalid binary digit {bad:?} in B'…' bit string"
21879                ))));
21880            }
21881            body
21882        };
21883        // Route through the postfix-cast loop so a chained cast like
21884        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
21885        // of erroring at the `::`.
21886        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
21887        // literal keeps its exact length, while an explicit `::bit` cast is
21888        // bit(1) with pad/truncate semantics (PG).
21889        Some(self.finish_postfix_casts(Expr::Cast {
21890            expr: Box::new(Expr::Literal(Literal::String(bits))),
21891            target: CastTarget::Named("__bit_literal".to_string()),
21892        }))
21893    }
21894
21895    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
21896        if let Some(res) = self.try_parse_bit_string_literal() {
21897            return res;
21898        }
21899        let tok_pos = self.pos;
21900        match self.advance() {
21901            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
21902            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
21903            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
21904            // carrying the source mantissa + scale so no precision is lost. A
21905            // literal too wide for i128 falls back to double precision.
21906            // Out-of-line (#[inline(never)]) — this arm sits on the
21907            // parse_expr recursion chain; its expansion locals must not
21908            // widen the recursive frame (debug frame-cliff discipline).
21909            Token::Numeric(s) => match numeric_token_to_literal(s) {
21910                Ok(lit) => Ok(Expr::Literal(lit)),
21911                Err(msg) => Err(self.err(msg)),
21912            },
21913            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
21914            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
21915            // (the lexer only emits this token in the MySQL dialect). Lower
21916            // onto the existing bytea cast; out-of-line to keep this arm off
21917            // the parse recursion frame.
21918            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
21919            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
21920            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
21921            Token::Null => Ok(Expr::Literal(Literal::Null)),
21922            // v6.1.1 — `$N` placeholder. The actual Value lookup
21923            // happens in the engine eval path against the prepared-
21924            // statement bind buffer.
21925            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
21926            Token::LParen => {
21927                // v4.10: `(SELECT ...)` in expression position is a
21928                // scalar subquery; otherwise it's a parenthesised
21929                // expression. Peek for SELECT keyword to dispatch.
21930                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
21931                // lexes as Ident("with") (not a reserved token). The subquery body
21932                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
21933                // so its large `Statement` local stays out of parse_atom's stack
21934                // frame — parse_atom is on the recursive `((…))` cycle and the
21935                // nesting budget is tuned to its frame size).
21936                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21937                    if s.eq_ignore_ascii_case("with"));
21938                if matches!(self.peek(), Token::Select) || is_with {
21939                    self.parse_paren_scalar_subquery(is_with)
21940                } else {
21941                    let e = self.parse_expr(0)?;
21942                    // `(a, b, …)` — a row constructor. Valid only
21943                    // in front of a comparison operator or [NOT]
21944                    // IN; both expand at parse time (lexicographic
21945                    // comparison / OR'd row equalities).
21946                    if matches!(self.peek(), Token::Comma) {
21947                        let mut row = alloc::vec![e];
21948                        while matches!(self.peek(), Token::Comma) {
21949                            self.advance();
21950                            row.push(self.parse_expr(0)?);
21951                        }
21952                        if !matches!(self.peek(), Token::RParen) {
21953                            return Err(self.err(alloc::format!(
21954                                "expected ')' after row constructor, got {:?}",
21955                                self.peek()
21956                            )));
21957                        }
21958                        self.advance();
21959                        // A bare `(a, b, …)` row constructor can carry postfix
21960                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
21961                        // early return here skips parse_atom's tail postfix
21962                        // pass, so fold casts in explicitly. For the
21963                        // comparison / predicate forms nothing postfix follows,
21964                        // so this is a no-op.
21965                        return self
21966                            .parse_row_comparison_tail(row)
21967                            .and_then(|e| self.finish_postfix_casts(e));
21968                    }
21969                    match self.advance() {
21970                        Token::RParen => Ok(e),
21971                        other => Err(ParseError {
21972                            message: format!("expected ')', got {other:?}"),
21973                            token_pos: self.consumed_pos(),
21974                        }),
21975                    }
21976                }
21977            }
21978            Token::LBracket => self.parse_vector_literal_body(),
21979            Token::Extract => self.parse_extract_atom(),
21980            Token::Interval => self.parse_interval_atom(),
21981            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
21982            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
21983            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
21984            // expression position calling the PG `left(string, n)` /
21985            // `right(string, n)` function; rebuild the AST as a regular
21986            // function call so the engine's apply_function dispatch picks
21987            // it up. Delegated to a #[inline(never)] helper so its locals
21988            // don't bloat this recursive `parse_atom` frame (the nesting
21989            // budget in `enter_nested` is tuned to parse_atom's size).
21990            Token::Left if matches!(self.peek(), Token::LParen) => {
21991                self.parse_lr_string_function_call("left")
21992            }
21993            Token::Right if matches!(self.peek(), Token::LParen) => {
21994                self.parse_lr_string_function_call("right")
21995            }
21996            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
21997            // token; we match on the bare ident. NOT is a token
21998            // (consumed in the comparison rung), but `EXISTS (...)`
21999            // at the top of an expression starts here.
22000            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22001                self.parse_exists_atom(false)
22002            }
22003            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22004            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22005            // CASE is a bare ident; we dispatch on lowercase match.
22006            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22007                self.parse_case_atom()
22008            }
22009            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22010            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22011            // '…'`. Lower onto the ::cast node so the existing
22012            // runtime text→date/timestamp paths do the parsing. The
22013            // string must follow immediately, else the ident stays a
22014            // plain column reference.
22015            Token::Ident(s)
22016                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22017                    && matches!(self.peek(), Token::String(_)) =>
22018            {
22019                let target =
22020                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22021                let Token::String(lit) = self.advance() else {
22022                    unreachable!("peek guaranteed a string token");
22023                };
22024                Ok(Expr::Cast {
22025                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22026                    target,
22027                })
22028            }
22029            // v7.39 (round 221) — the SQL-standard long spellings:
22030            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22031            // TIME ZONE '…'`. Consume the modifier and lower to the same
22032            // typed-literal cast (`timetz` / `timestamptz` for WITH).
22033            Token::Ident(s)
22034                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22035                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22036                        || w.eq_ignore_ascii_case("without"))
22037                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22038                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22039                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22040            {
22041                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22042                self.advance(); // WITH / WITHOUT
22043                self.advance(); // TIME
22044                self.advance(); // ZONE
22045                let Token::String(lit) = self.advance() else {
22046                    unreachable!("guard checked a string token");
22047                };
22048                let base = s.to_ascii_lowercase();
22049                let target = match (base.as_str(), with_tz) {
22050                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22051                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22052                    (_, true) => CastTarget::Timestamptz,
22053                    (_, false) => CastTarget::Timestamp,
22054                };
22055                Ok(Expr::Cast {
22056                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22057                    target,
22058                })
22059            }
22060            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22061            // gathers the subquery's single-column rows (in its row order)
22062            // into an array. Desugared to `array_agg` over the subquery as a
22063            // derived table; out-of-line to keep parse_atom's frame small (it
22064            // sits on the recursive nesting-budget cycle).
22065            Token::Ident(s) | Token::QuotedIdent(s)
22066                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22067            {
22068                self.parse_array_subquery()
22069            }
22070            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22071            // is not a reserved token; we match by case-insensitive
22072            // ident. The opening `[` must follow immediately. v7.39 (read01
22073            // round 105) — the body moved out-of-line so its `Vec`/loop locals
22074            // leave parse_atom's frame (which sits on the nesting-budget cycle).
22075            Token::Ident(s) | Token::QuotedIdent(s)
22076                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22077            {
22078                self.parse_array_literal_body()
22079            }
22080            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22081            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22082            // We special-case before the generic ident dispatch so
22083            // the AGAINST clause never reaches the function-call
22084            // loop (which would mis-read `(cols) AGAINST` as a
22085            // call with no trailing modifier). The shape is
22086            // rewritten to a Boolean OR over per-column
22087            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22088            // term)` so the existing FTS evaluator handles
22089            // semantics — the fulltext-GIN built at CREATE TABLE
22090            // time is currently a "real index that survives dump
22091            // round-trip"; the planner hook that actually uses
22092            // it for posting-list intersection lands in a later
22093            // sub-phase (Phase 2.2b) without touching this surface.
22094            Token::Ident(s) | Token::QuotedIdent(s)
22095                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22096            {
22097                self.parse_match_against_atom()
22098            }
22099            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22100            // v7.37.43-T4 — PG-unreserved keywords are legal column /
22101            // alias names in expression context too. `release` appears
22102            // in sentori `0003_partition_events.sql` as both a column
22103            // reference (SELECT … release …) and an INSERT column list
22104            // entry. Mirrors `expect_ident_like`'s expansion of the
22105            // identifier set.
22106            other if unreserved_keyword_text(&other).is_some() => {
22107                let s = unreserved_keyword_text(&other).unwrap();
22108                self.finish_ident_atom(s)
22109            }
22110            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22111            // only inside `SET` before, so `SELECT @@autocommit` — which
22112            // every MySQL connector asks at handshake — was a parse error.
22113            // MariaDB accepts the bare, `@@session.` and `@@global.`
22114            // spellings alike and answers from the session's own value.
22115            Token::SessionVar(v) => {
22116                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22117                // has nothing to do with a `@@` engine setting: its own
22118                // per-session namespace, and an unset one reads NULL instead
22119                // of raising. Stripping every `@` (as this did) made `@x` and
22120                // `@@x` the same node, so `SELECT @x` answered "Unknown
22121                // system variable".
22122                Ok(variable_ref_atom(&v))
22123            }
22124            other => Err(ParseError {
22125                message: format!("unexpected token {other:?} in expression"),
22126                token_pos: tok_pos,
22127            }),
22128        }
22129        // After parsing the atom, fold any postfix `::vector` casts.
22130        .and_then(|atom| self.finish_postfix_casts(atom))
22131    }
22132
22133    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22134    /// Both bind tighter than any binary op.
22135    /// Shared cast-target parser for postfix `::TYPE` and the
22136    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22137    /// If the next tokens are `( N )`, consume them and return the canonical
22138    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22139    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22140    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22141        if !matches!(self.peek(), Token::LParen) {
22142            return None;
22143        }
22144        self.advance(); // (
22145        let n = match self.advance() {
22146            Token::Integer(n) => n,
22147            _ => return Some(base.to_string()), // malformed → drop precision
22148        };
22149        if matches!(self.peek(), Token::RParen) {
22150            self.advance();
22151        }
22152        Some(alloc::format!("{base}({n})"))
22153    }
22154
22155    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22156        // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22157        // schema-qualifies every cast target, and `pg_catalog.X` names
22158        // exactly the builtin type X. Consume the qualifier and let
22159        // the ordinary target parse decide.
22160        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22161            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22162        {
22163            self.advance();
22164            self.advance();
22165        }
22166        let target = match self.advance() {
22167            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22168                "int" | "integer" | "int4" => {
22169                    if matches!(self.peek(), Token::LBracket)
22170                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22171                    {
22172                        self.advance();
22173                        self.advance();
22174                        CastTarget::IntArray
22175                    } else {
22176                        CastTarget::Int
22177                    }
22178                }
22179                "bigint" | "int8" => {
22180                    if matches!(self.peek(), Token::LBracket)
22181                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22182                    {
22183                        self.advance();
22184                        self.advance();
22185                        CastTarget::BigIntArray
22186                    } else {
22187                        CastTarget::BigInt
22188                    }
22189                }
22190                "float" | "double" => CastTarget::Float,
22191                "text" => {
22192                    // v7.10.11 — `::TEXT[]` widens to TextArray.
22193                    if matches!(self.peek(), Token::LBracket)
22194                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22195                    {
22196                        self.advance();
22197                        self.advance();
22198                        CastTarget::TextArray
22199                    } else {
22200                        CastTarget::Text
22201                    }
22202                }
22203                "bool" | "boolean" => CastTarget::Bool,
22204                "vector" => CastTarget::Vector,
22205                "date" => CastTarget::Date,
22206                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22207                // seconds precision through the Named path (the engine rounds
22208                // the sub-second field); bare `::timestamp` keeps the fast arm.
22209                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22210                    Some(named) => CastTarget::Named(named),
22211                    None => CastTarget::Timestamp,
22212                },
22213                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22214                    Some(named) => CastTarget::Named(named),
22215                    None => CastTarget::Timestamptz,
22216                },
22217                "interval" => CastTarget::Interval,
22218                "json" => CastTarget::Json,
22219                "jsonb" => CastTarget::Jsonb,
22220                // v7.39 (round 694) — these have dedicated CastTarget
22221                // variants, so they never reached the postfix `[]` handling
22222                // further down and `::regtype[]` was a SYNTAX error at the
22223                // `]`. PG has an array type for every scalar; take the
22224                // suffix here and hand the canonical `<ty>_array` name to
22225                // the engine, the same shape every other array cast uses.
22226                "regtype" if self.peek_postfix_array_brackets() => {
22227                    self.advance();
22228                    self.advance();
22229                    CastTarget::Named(alloc::string::String::from("regtype_array"))
22230                }
22231                "regclass" if self.peek_postfix_array_brackets() => {
22232                    self.advance();
22233                    self.advance();
22234                    CastTarget::Named(alloc::string::String::from("regclass_array"))
22235                }
22236                "regtype" => CastTarget::RegType,
22237                "regclass" => CastTarget::RegClass,
22238                // v7.12.0 — `::tsvector` / `::tsquery`.
22239                // Engine decodes the LHS text via the PG
22240                // external form parser.
22241                // v7.39 (round 352, M8) — MySQL's own cast targets.
22242                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22243                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22244                // such type, so they are taken only in that dialect and
22245                // fall through to the "type does not exist" arm otherwise.
22246                "signed" | "unsigned" if self.mysql_dialect => {
22247                    if matches!(self.peek(), Token::Ident(k)
22248                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22249                    {
22250                        self.advance();
22251                    }
22252                    CastTarget::Named(s.to_ascii_lowercase())
22253                }
22254                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22255                // in MySQL: MariaDB answers '123' where the SQL-standard
22256                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22257                // Truncating a number to its first digit is a wrong answer
22258                // with no error, so the MySQL session gets MySQL's reading.
22259                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22260                    CastTarget::Text
22261                }
22262                "tsvector" => CastTarget::TsVector,
22263                "tsquery" => CastTarget::TsQuery,
22264                // v7.17.0 — `::uuid`. Engine decodes the LHS
22265                // text via `spg_storage::parse_uuid_str`.
22266                "uuid" => CastTarget::Uuid,
22267                // v7.18 — `::bytea`. Engine decodes the LHS
22268                // text via the PG hex form (`'\xdeadbeef'`)
22269                // or escape form (`'\\x05\\x00'`). Closes
22270                // mailrs D-pre #3 reverse-acceptance gap.
22271                "bytea" => CastTarget::Bytea,
22272                // v7.37.5 ship triage — generic typed-cast escape.
22273                // Anything the long-tail PG type ident table knows
22274                // about(network/bit/geometry/multirange/etc.)flows
22275                // through `CastTarget::Named(canonical)`; the engine
22276                // resolves via `column_type_to_data_type` and dispatches
22277                // through the typed `coerce_value` path. Truly
22278                // unrecognised idents still hit the error arm below
22279                // because the engine rejects them.
22280                other => {
22281                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22282                    // `::varchar(255)`, etc. Capture into the canonical
22283                    // `name(p,s)` form so `type_name_to_data_type` can
22284                    // reconstruct the `DataType::Numeric { precision,
22285                    // scale }` (and similar param-carrying types).
22286                    let mut name = other.to_string();
22287                    // v7.39 (round 281) — `::bit varying(3)` is two
22288                    // words; fold the tail in so the typmod reaches the
22289                    // type resolver instead of tripping the parser.
22290                    if name.eq_ignore_ascii_case("bit")
22291                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22292                    {
22293                        self.advance();
22294                        name = alloc::string::String::from("varbit");
22295                    }
22296                    // v7.39 (round 613) — `::character varying` is the same
22297                    // two-word shape and had no fold, so the `varying` was
22298                    // left behind and the cast became a bare `character`,
22299                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22300                    // `a` where PG answers `ab`. Silently, and for a spelling
22301                    // pg_dump writes.
22302                    if name.eq_ignore_ascii_case("character")
22303                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22304                    {
22305                        self.advance();
22306                        name = alloc::string::String::from("varchar");
22307                    }
22308                    if matches!(self.peek(), Token::LParen) {
22309                        let mut buf = alloc::string::String::from("(");
22310                        let mut depth = 0usize;
22311                        loop {
22312                            match self.advance() {
22313                                Token::LParen => {
22314                                    depth += 1;
22315                                    if depth > 1 {
22316                                        buf.push('(');
22317                                    }
22318                                }
22319                                Token::RParen => {
22320                                    depth -= 1;
22321                                    if depth == 0 {
22322                                        buf.push(')');
22323                                        break;
22324                                    }
22325                                    buf.push(')');
22326                                }
22327                                Token::Comma => buf.push(','),
22328                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22329                                // v7.39 (round 273) — a minus used to fall
22330                                // into the catch-all below and vanish, so
22331                                // `::numeric(10,-2)` reached the engine as
22332                                // the text `numeric(10,2)` and silently
22333                                // rounded to two DECIMALS instead of to
22334                                // hundreds. A dropped token is not a
22335                                // no-op when it carries a sign.
22336                                Token::Minus => buf.push('-'),
22337                                Token::Eof => break,
22338                                _ => {}
22339                            }
22340                        }
22341                        name.push_str(&buf);
22342                    }
22343                    // Optional postfix `[]` widens to the array form —
22344                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22345                    // The engine's `type_name_to_data_type` recognises
22346                    // the canonical `<ty>_array` form.
22347                    if matches!(self.peek(), Token::LBracket)
22348                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22349                    {
22350                        self.advance();
22351                        self.advance();
22352                        name.push_str("_array");
22353                    }
22354                    CastTarget::Named(name)
22355                }
22356            },
22357            Token::Interval => CastTarget::Interval,
22358            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22359            // "char" (oid 18, SPG Char1 — distinct from bare `char`
22360            // = char(1)); other quoted names resolve like idents.
22361            Token::QuotedIdent(q) => {
22362                if q.eq_ignore_ascii_case("char") {
22363                    CastTarget::Named("char1".into())
22364                } else {
22365                    CastTarget::Named(q.to_ascii_lowercase())
22366                }
22367            }
22368            other => {
22369                return Err(ParseError {
22370                    message: format!("expected type ident after `::`, got {other:?}"),
22371                    token_pos: self.consumed_pos(),
22372                });
22373            }
22374        };
22375        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22376        // target to its array sibling. Closed-enum arms (Bool /
22377        // SmallInt / Numeric / Float / Date / …) didn't carry the
22378        // explicit widening that Text / Int / BigInt did, so
22379        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22380        // error. The widening here mirrors the per-arm Text /
22381        // Int / BigInt logic above + folds the new ζ-A first-class
22382        // types through `CastTarget::Named("<ty>_array")`.
22383        if matches!(self.peek(), Token::LBracket)
22384            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22385        {
22386            let widened = match &target {
22387                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22388                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22389                // v7.39 (round 326, V43) — the two temporal types stay
22390                // distinct. Both used to widen to `timestamptz_array`, so
22391                // `::timestamp[]` named the wrong target in its own error
22392                // message and lost the zone-less identity on the way.
22393                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22394                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22395                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22396                CastTarget::Json | CastTarget::Jsonb => {
22397                    Some(CastTarget::Named("jsonb_array".to_string()))
22398                }
22399                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22400                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22401                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22402                CastTarget::Named(name) => {
22403                    let mut a = name.clone();
22404                    a.push_str("_array");
22405                    Some(CastTarget::Named(a))
22406                }
22407                // Int / BigInt / Text / Vector / TsVector / TsQuery /
22408                // RegType / RegClass / TextArray / IntArray /
22409                // BigIntArray already finalised — leave as is.
22410                _ => None,
22411            };
22412            if let Some(w) = widened {
22413                self.advance();
22414                self.advance();
22415                return Ok(w);
22416            }
22417        }
22418        Ok(target)
22419    }
22420
22421    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22422        loop {
22423            // v7.38 (read01, T9) — composite field access `(expr).field`.
22424            // A bare `a.b` is consumed as a qualified column inside the ident
22425            // atom, so a Dot only survives to this postfix position when the
22426            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22427            // `.*` whole-row expansion is not handled here (projection-level).
22428            if matches!(self.peek(), Token::Dot)
22429                && matches!(
22430                    self.tokens.get(self.pos + 1),
22431                    Some(Token::Ident(_) | Token::QuotedIdent(_))
22432                )
22433            {
22434                self.advance(); // .
22435                let field = match self.advance() {
22436                    Token::Ident(s) | Token::QuotedIdent(s) => s,
22437                    other => {
22438                        return Err(
22439                            self.err(format!("expected a field name after '.', got {other:?}"))
22440                        );
22441                    }
22442                };
22443                expr = Expr::FieldAccess {
22444                    base: Box::new(expr),
22445                    field,
22446                };
22447                continue;
22448            }
22449            if matches!(self.peek(), Token::DoubleColon) {
22450                self.advance();
22451                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22452                // target set to include INTERVAL (reserved Token),
22453                // TIMESTAMPTZ, and PG catalog regtype / regclass.
22454                // mailrs follow-up H3a + H3b.
22455                let target = self.parse_cast_target()?;
22456                expr = Expr::Cast {
22457                    expr: Box::new(expr),
22458                    target,
22459                };
22460                continue;
22461            }
22462            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22463            // returns NULL for out-of-range. Multiple subscripts
22464            // chain: `a[i][j]` parses left-to-right.
22465            if matches!(self.peek(), Token::LBracket) {
22466                self.advance();
22467                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22468                // bare index stays a subscript.
22469                let lo = if matches!(self.peek(), Token::Colon) {
22470                    None
22471                } else {
22472                    Some(self.parse_expr(0)?)
22473                };
22474                if matches!(self.peek(), Token::Colon) {
22475                    self.advance();
22476                    let hi = if matches!(self.peek(), Token::RBracket) {
22477                        None
22478                    } else {
22479                        Some(Box::new(self.parse_expr(0)?))
22480                    };
22481                    if !matches!(self.peek(), Token::RBracket) {
22482                        return Err(self.err(alloc::format!(
22483                            "expected ']' after array slice, got {:?}",
22484                            self.peek()
22485                        )));
22486                    }
22487                    self.advance();
22488                    expr = Expr::ArraySlice {
22489                        target: Box::new(expr),
22490                        lo: lo.map(Box::new),
22491                        hi,
22492                    };
22493                    continue;
22494                }
22495                let index = lo.expect("non-colon branch parsed an index");
22496                if !matches!(self.peek(), Token::RBracket) {
22497                    return Err(self.err(alloc::format!(
22498                        "expected ']' after array index, got {:?}",
22499                        self.peek()
22500                    )));
22501                }
22502                self.advance();
22503                expr = Expr::ArraySubscript {
22504                    target: Box::new(expr),
22505                    index: Box::new(index),
22506                };
22507                continue;
22508            }
22509            // `expr AT TIME ZONE zone` — lowers to PG's own function
22510            // form timezone(zone, expr); the scalar implements the
22511            // offset shift (named zones error there — no tzdata).
22512            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22513                && matches!(self.tokens.get(self.pos + 1),
22514                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22515                && matches!(self.tokens.get(self.pos + 2),
22516                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22517            {
22518                self.advance(); // AT
22519                self.advance(); // TIME
22520                self.advance(); // ZONE
22521                // Zone at comparison precedence so AND/OR stay out.
22522                let zone = self.parse_expr(6)?;
22523                expr = Expr::FunctionCall {
22524                    name: "timezone".to_string(),
22525                    args: alloc::vec![zone, expr],
22526                };
22527                continue;
22528            }
22529            // `expr COLLATE "name"` — SPG's single text ordering IS
22530            // byte order, i.e. the C collation. The byte-order
22531            // spellings absorb as no-ops; a locale collation would
22532            // silently sort differently from PG, so it errors
22533            // honestly instead.
22534            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22535                self.advance();
22536                let mut cname = match self.advance() {
22537                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22538                    other => {
22539                        return Err(self.err(alloc::format!(
22540                            "expected collation name after COLLATE, got {other:?}"
22541                        )));
22542                    }
22543                };
22544                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22545                // is how `pg_dump` writes the default one:
22546                // `… COLLATE pg_catalog.default`. Reading a single token
22547                // left the SCHEMA as the name, so the clause was refused
22548                // as an unsupported locale collation and no dump ran.
22549                if matches!(self.peek(), Token::Dot) {
22550                    self.advance();
22551                    cname = match self.advance() {
22552                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22553                        // `default` lexes as a KEYWORD, and it is the name
22554                        // pg_dump writes — the same trap round 535 hit with
22555                        // TABLE / INDEX / FULL.
22556                        Token::Default => alloc::string::String::from("default"),
22557                        other => {
22558                            return Err(self.err(alloc::format!(
22559                                "expected collation name after COLLATE, got {other:?}"
22560                            )));
22561                        }
22562                    };
22563                }
22564                let lc = cname.to_ascii_lowercase();
22565                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22566                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22567                // family / `binary`) forces byte-wise, which is exactly
22568                // what `BINARY expr` does — lower onto that so every fold
22569                // site (comparison, LIKE, ORDER BY) suppresses via
22570                // `is_binary_coerced`. A `_ci` family override folds, and
22571                // under the MySQL dialect the default already folds, so it
22572                // absorbs as a no-op; likewise the C / byte-order spellings.
22573                if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22574                    expr = Expr::Cast {
22575                        expr: alloc::boxed::Box::new(expr),
22576                        target: CastTarget::Named("binary".to_string()),
22577                    };
22578                    continue;
22579                }
22580                let mysql_ci = self.mysql_dialect
22581                    && (lc.ends_with("_ci")
22582                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22583                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22584                // goes to the lowering channel, the byte-order spellings
22585                // included. Round 691 recorded only the names the old
22586                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22587                // absorbed as a no-op — and once a column could declare a
22588                // collation, absorbing the clause meant the COLUMN's
22589                // collation won where the query had asked for bytes.
22590                if self.in_order_by_key && !mysql_ci {
22591                    self.order_key_collation = Some(cname);
22592                    continue;
22593                }
22594                if !matches!(
22595                    lc.as_str(),
22596                    "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22597                ) && !mysql_ci
22598                {
22599                    return Err(self.err(alloc::format!(
22600                        "COLLATE {cname:?}: SPG orders text by bytes (the C \
22601                         collation); locale collations are not supported yet — \
22602                         use COLLATE \"C\" or drop the clause"
22603                    )));
22604                }
22605                continue;
22606            }
22607            return Ok(expr);
22608        }
22609    }
22610
22611    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22612    /// the first token that is not one. Schema qualifiers collapse to the
22613    /// last part, which is what every other name path here does (SPG is
22614    /// single-schema).
22615    fn take_comma_separated_names(&mut self) -> Vec<String> {
22616        let mut out = Vec::new();
22617        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22618            self.advance();
22619            let mut last = n;
22620            while matches!(self.peek(), Token::Dot) {
22621                self.advance();
22622                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22623                    last = t;
22624                }
22625            }
22626            out.push(last);
22627            if matches!(self.peek(), Token::Comma) {
22628                self.advance();
22629            } else {
22630                break;
22631            }
22632        }
22633        out
22634    }
22635
22636    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22637    ///
22638    /// The general cast-target path tests this inline; the types with their
22639    /// own `CastTarget` variant need it as a guard on their match arm,
22640    /// which is what this exists for.
22641    fn peek_postfix_array_brackets(&self) -> bool {
22642        matches!(self.peek(), Token::LBracket)
22643            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22644    }
22645
22646    /// Parse the operator tail after a `(a, b, …)` row constructor
22647    /// and expand at parse time. `=` is the conjunction of element
22648    /// equalities; `<>` its negation; the order operators expand
22649    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22650    /// equalities. Anything else (a bare row value, a subquery
22651    /// RHS) errors honestly — SPG has no composite runtime value.
22652    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22653        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22654            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22655                lhs: Box::new(l.clone()),
22656                op: BinOp::Eq,
22657                rhs: Box::new(r.clone()),
22658            });
22659            let first = it.next().expect("row has at least two elements");
22660            it.fold(first, |acc, e| Expr::Binary {
22661                lhs: Box::new(acc),
22662                op: BinOp::And,
22663                rhs: Box::new(e),
22664            })
22665        }
22666        // Lexicographic (a,b) OP (c,d):
22667        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
22668        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22669            if lhs.len() == 1 {
22670                return Expr::Binary {
22671                    lhs: Box::new(lhs[0].clone()),
22672                    op: last,
22673                    rhs: Box::new(rhs[0].clone()),
22674                };
22675            }
22676            let head_strict = Expr::Binary {
22677                lhs: Box::new(lhs[0].clone()),
22678                op: strict,
22679                rhs: Box::new(rhs[0].clone()),
22680            };
22681            let head_eq = Expr::Binary {
22682                lhs: Box::new(lhs[0].clone()),
22683                op: BinOp::Eq,
22684                rhs: Box::new(rhs[0].clone()),
22685            };
22686            Expr::Binary {
22687                lhs: Box::new(head_strict),
22688                op: BinOp::Or,
22689                rhs: Box::new(Expr::Binary {
22690                    lhs: Box::new(head_eq),
22691                    op: BinOp::And,
22692                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22693                }),
22694            }
22695        }
22696        let negated_in = if matches!(self.peek(), Token::Not)
22697            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22698        {
22699            self.advance();
22700            true
22701        } else {
22702            false
22703        };
22704        if matches!(self.peek(), Token::In) {
22705            self.advance();
22706            if !matches!(self.peek(), Token::LParen) {
22707                return Err(self.err(alloc::format!(
22708                    "expected '(' after row IN, got {:?}",
22709                    self.peek()
22710                )));
22711            }
22712            self.advance();
22713            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22714            // not a list of literal rows. Row-vs-list decomposes to
22715            // OR-of-AND above, but the subquery's rows are only known at
22716            // runtime, so keep it as a RowInSubquery node.
22717            if matches!(self.peek(), Token::Select) {
22718                let inner = self.parse_select_stmt()?;
22719                if !matches!(self.peek(), Token::RParen) {
22720                    return Err(self.err(alloc::format!(
22721                        "expected ')' after row IN-subquery, got {:?}",
22722                        self.peek()
22723                    )));
22724                }
22725                self.advance();
22726                let Statement::Select(s) = inner else {
22727                    unreachable!("parse_select_stmt always returns Statement::Select")
22728                };
22729                return Ok(Expr::RowInSubquery {
22730                    row,
22731                    subquery: Box::new(s),
22732                    negated: negated_in,
22733                });
22734            }
22735            let mut alternatives: Vec<Expr> = Vec::new();
22736            loop {
22737                // Optional ROW keyword before the paren row.
22738                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22739                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22740                {
22741                    self.advance();
22742                }
22743                if !matches!(self.peek(), Token::LParen) {
22744                    return Err(self.err(alloc::format!(
22745                        "expected '(' to open a row inside IN, got {:?}",
22746                        self.peek()
22747                    )));
22748                }
22749                self.advance();
22750                let mut rhs = alloc::vec![self.parse_expr(0)?];
22751                while matches!(self.peek(), Token::Comma) {
22752                    self.advance();
22753                    rhs.push(self.parse_expr(0)?);
22754                }
22755                if !matches!(self.peek(), Token::RParen) {
22756                    return Err(self.err(alloc::format!(
22757                        "expected ')' after row inside IN, got {:?}",
22758                        self.peek()
22759                    )));
22760                }
22761                self.advance();
22762                if rhs.len() != row.len() {
22763                    return Err(self.err(alloc::format!(
22764                        "row IN arity mismatch: left has {}, right has {}",
22765                        row.len(),
22766                        rhs.len()
22767                    )));
22768                }
22769                alternatives.push(row_eq(&row, &rhs));
22770                if matches!(self.peek(), Token::Comma) {
22771                    self.advance();
22772                    continue;
22773                }
22774                break;
22775            }
22776            if !matches!(self.peek(), Token::RParen) {
22777                return Err(self.err(alloc::format!(
22778                    "expected ')' to close row IN list, got {:?}",
22779                    self.peek()
22780                )));
22781            }
22782            self.advance();
22783            let mut it = alternatives.into_iter();
22784            let first = it.next().expect("IN list has at least one row");
22785            let combined = it.fold(first, |acc, e| Expr::Binary {
22786                lhs: Box::new(acc),
22787                op: BinOp::Or,
22788                rhs: Box::new(e),
22789            });
22790            return Ok(maybe_not(combined, negated_in));
22791        }
22792        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
22793        // two periods share at least one time point. Each pair is
22794        // normalised with least/greatest (PG accepts the endpoints
22795        // in either order), then lowered to the standard
22796        // `start1 < end2 AND start2 < end1` form.
22797        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
22798            if row.len() != 2 {
22799                return Err(self.err(alloc::format!(
22800                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
22801                    row.len()
22802                )));
22803            }
22804            self.advance();
22805            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22806                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22807            {
22808                self.advance();
22809            }
22810            if !matches!(self.peek(), Token::LParen) {
22811                return Err(self.err(alloc::format!(
22812                    "expected '(' after OVERLAPS, got {:?}",
22813                    self.peek()
22814                )));
22815            }
22816            self.advance();
22817            let r0 = self.parse_expr(0)?;
22818            if !matches!(self.peek(), Token::Comma) {
22819                return Err(self.err(alloc::format!(
22820                    "OVERLAPS needs (start, end) on the right, got {:?}",
22821                    self.peek()
22822                )));
22823            }
22824            self.advance();
22825            let r1 = self.parse_expr(0)?;
22826            if !matches!(self.peek(), Token::RParen) {
22827                return Err(self.err(alloc::format!(
22828                    "expected ')' after OVERLAPS pair, got {:?}",
22829                    self.peek()
22830                )));
22831            }
22832            self.advance();
22833            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
22834                name: String::from(name),
22835                args: alloc::vec![a.clone(), b.clone()],
22836            };
22837            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
22838                lhs: Box::new(lhs),
22839                op: BinOp::Lt,
22840                rhs: Box::new(rhs),
22841            };
22842            return Ok(Expr::Binary {
22843                lhs: Box::new(lt(
22844                    pair_fn("least", &row[0], &row[1]),
22845                    pair_fn("greatest", &r0, &r1),
22846                )),
22847                op: BinOp::And,
22848                rhs: Box::new(lt(
22849                    pair_fn("least", &r0, &r1),
22850                    pair_fn("greatest", &row[0], &row[1]),
22851                )),
22852            });
22853        }
22854        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
22855        // PG, `IS NULL` is true only when EVERY field is NULL, and
22856        // `IS NOT NULL` is true only when every field is non-NULL — the
22857        // latter is NOT the negation of the former (a mixed row is
22858        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
22859        // which reproduces exactly that all-fields semantics.
22860        if matches!(self.peek(), Token::Is) {
22861            self.advance();
22862            let negated = if matches!(self.peek(), Token::Not) {
22863                self.advance();
22864                true
22865            } else {
22866                false
22867            };
22868            if !matches!(self.peek(), Token::Null) {
22869                return Err(self.err(alloc::format!(
22870                    "expected NULL after row IS [NOT], got {:?}",
22871                    self.peek()
22872                )));
22873            }
22874            self.advance();
22875            let mut it = row.iter().map(|e| Expr::IsNull {
22876                expr: Box::new(e.clone()),
22877                negated,
22878            });
22879            let first = it.next().expect("row has at least two elements");
22880            return Ok(it.fold(first, |acc, e| Expr::Binary {
22881                lhs: Box::new(acc),
22882                op: BinOp::And,
22883                rhs: Box::new(e),
22884            }));
22885        }
22886        let op = match self.peek() {
22887            Token::Eq => BinOp::Eq,
22888            Token::NotEq => BinOp::NotEq,
22889            Token::Lt => BinOp::Lt,
22890            Token::LtEq => BinOp::LtEq,
22891            Token::Gt => BinOp::Gt,
22892            Token::GtEq => BinOp::GtEq,
22893            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
22894            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
22895            // constructor value, identical to the `ROW(a, b, …)` keyword form:
22896            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
22897            // (`::text`, `.field`) applies at the caller just as it does for the
22898            // ROW(...) node. All the comparison / predicate forms returned above.
22899            _ => {
22900                return Ok(Expr::FunctionCall {
22901                    name: String::from("row"),
22902                    args: row,
22903                });
22904            }
22905        };
22906        self.advance();
22907        // Optional ROW keyword before the paren row.
22908        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22909            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22910        {
22911            self.advance();
22912        }
22913        if !matches!(self.peek(), Token::LParen) {
22914            return Err(self.err(alloc::format!(
22915                "expected '(' to open the right-hand row, got {:?}",
22916                self.peek()
22917            )));
22918        }
22919        self.advance();
22920        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
22921        // subquery. Kept as a node (the subquery's row is a runtime value);
22922        // the literal-RHS form below still decomposes at parse time.
22923        if matches!(self.peek(), Token::Select) {
22924            let inner = self.parse_select_stmt()?;
22925            if !matches!(self.peek(), Token::RParen) {
22926                return Err(self.err(alloc::format!(
22927                    "expected ')' after row comparison subquery, got {:?}",
22928                    self.peek()
22929                )));
22930            }
22931            self.advance();
22932            let Statement::Select(s) = inner else {
22933                unreachable!("parse_select_stmt always returns Statement::Select")
22934            };
22935            return Ok(Expr::RowCmpSubquery {
22936                row,
22937                op,
22938                subquery: Box::new(s),
22939            });
22940        }
22941        let mut rhs = alloc::vec![self.parse_expr(0)?];
22942        while matches!(self.peek(), Token::Comma) {
22943            self.advance();
22944            rhs.push(self.parse_expr(0)?);
22945        }
22946        if !matches!(self.peek(), Token::RParen) {
22947            return Err(self.err(alloc::format!(
22948                "expected ')' after right-hand row, got {:?}",
22949                self.peek()
22950            )));
22951        }
22952        self.advance();
22953        if rhs.len() != row.len() {
22954            // v7.39 (round 239) — PG's wording (42601).
22955            return Err(self.err("unequal number of entries in row expressions".to_string()));
22956        }
22957        Ok(match op {
22958            BinOp::Eq => row_eq(&row, &rhs),
22959            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
22960            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
22961            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
22962            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
22963            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
22964            _ => unreachable!("op restricted above"),
22965        })
22966    }
22967
22968    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
22969    /// escape character becomes the matcher's default backslash:
22970    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
22971    /// → the char itself, and any pre-existing backslash escapes
22972    /// itself so it stays literal. Both operands must be string
22973    /// literals — a runtime pattern would need matcher support.
22974    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
22975        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
22976            (&pattern, &esc)
22977        else {
22978            return Err(
22979                "LIKE ... ESCAPE requires string-literal pattern and escape \
22980                 (runtime escape characters are not supported yet)"
22981                    .into(),
22982            );
22983        };
22984        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
22985        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
22986        // multi-character escape is an error.
22987        let esc_ch: Option<char> = {
22988            let mut ch_iter = e.chars();
22989            match (ch_iter.next(), ch_iter.next()) {
22990                (Some(c), None) => Some(c),
22991                (None, _) => None,
22992                (Some(_), Some(_)) => {
22993                    return Err(alloc::format!(
22994                        "ESCAPE must be a single character, got {e:?}"
22995                    ));
22996                }
22997            }
22998        };
22999        let mut out = String::with_capacity(p.len() + 4);
23000        let mut chars = p.chars();
23001        while let Some(c) = chars.next() {
23002            if Some(c) == esc_ch {
23003                match chars.next() {
23004                    // Escaped wildcard / escaped escape → keep the
23005                    // next char literal via backslash.
23006                    Some(next) => {
23007                        out.push('\\');
23008                        out.push(next);
23009                    }
23010                    None => {
23011                        return Err("LIKE pattern ends with the escape character".into());
23012                    }
23013                }
23014            } else if c == '\\' && esc_ch != Some('\\') {
23015                // A raw backslash is literal under a custom (or absent) escape
23016                // — escape it for the backslash-based matcher.
23017                out.push('\\');
23018                out.push('\\');
23019            } else {
23020                out.push(c);
23021            }
23022        }
23023        Ok(Expr::Literal(Literal::String(out)))
23024    }
23025
23026    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
23027    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
23028    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
23029    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
23030    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
23031    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
23032    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
23033    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
23034    /// array expression errors honestly rather than silently mismatching.
23035    fn try_like_any_all(
23036        &mut self,
23037        base: &Expr,
23038        negated: bool,
23039        case_insensitive: bool,
23040    ) -> Result<Option<Expr>, ParseError> {
23041        let is_any = match self.peek() {
23042            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
23043            Token::Ident(s)
23044                if s.eq_ignore_ascii_case("any")
23045                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
23046            {
23047                true
23048            }
23049            _ => return Ok(None),
23050        };
23051        self.advance(); // ANY / ALL
23052        self.advance(); // '('
23053        let arr = self.parse_expr(0)?;
23054        if !matches!(self.peek(), Token::RParen) {
23055            return Err(self.err(format!(
23056                "expected ')' after LIKE {} argument, got {:?}",
23057                if is_any { "ANY" } else { "ALL" },
23058                self.peek()
23059            )));
23060        }
23061        self.advance(); // ')'
23062        let Expr::Array(items) = arr else {
23063            return Err(self.err(
23064                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23065            ));
23066        };
23067        let mut clauses = items.into_iter().map(|p| Expr::Like {
23068            expr: Box::new(base.clone()),
23069            pattern: Box::new(p),
23070            negated,
23071            case_insensitive,
23072        });
23073        let Some(first) = clauses.next() else {
23074            // ANY(empty) = FALSE, ALL(empty) = TRUE.
23075            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23076        };
23077        let op = if is_any { BinOp::Or } else { BinOp::And };
23078        let combined = clauses.fold(first, |acc, c| Expr::Binary {
23079            lhs: Box::new(acc),
23080            op,
23081            rhs: Box::new(c),
23082        });
23083        Ok(Some(combined))
23084    }
23085
23086    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
23087    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23088    /// `AND` is not swallowed.
23089    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23090        self.advance(); // BETWEEN
23091        // SYMMETRIC — the bounds may arrive in either order; both
23092        // orientations OR together. ASYMMETRIC is the default and
23093        // absorbs as noise.
23094        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23095        {
23096            self.advance();
23097            true
23098        } else {
23099            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23100                self.advance();
23101            }
23102            false
23103        };
23104        let low = self.parse_expr(6)?;
23105        if !matches!(self.peek(), Token::And) {
23106            return Err(self.err(format!(
23107                "expected AND after BETWEEN low bound, got {:?}",
23108                self.peek()
23109            )));
23110        }
23111        self.advance();
23112        let high = self.parse_expr(6)?;
23113        let target = Box::new(expr);
23114        let range = |lo: Expr, hi: Expr| Expr::Binary {
23115            lhs: Box::new(Expr::Binary {
23116                lhs: target.clone(),
23117                op: BinOp::GtEq,
23118                rhs: Box::new(lo),
23119            }),
23120            op: BinOp::And,
23121            rhs: Box::new(Expr::Binary {
23122                lhs: target.clone(),
23123                op: BinOp::LtEq,
23124                rhs: Box::new(hi),
23125            }),
23126        };
23127        let combined = if symmetric {
23128            Expr::Binary {
23129                lhs: Box::new(range(low.clone(), high.clone())),
23130                op: BinOp::Or,
23131                rhs: Box::new(range(high, low)),
23132            }
23133        } else {
23134            range(low, high)
23135        };
23136        Ok(maybe_not(combined, negated))
23137    }
23138
23139    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
23140    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23141    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23142    /// Caller already consumed the leading `WITH` ident.
23143    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23144    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23145    /// self-reference that appears more than once in a single term.
23146    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23147        use crate::ast::{CteBody, SelectStatement};
23148        if !cte.recursive {
23149            return Ok(());
23150        }
23151        let CteBody::Select(body) = &cte.body else {
23152            return Ok(());
23153        };
23154        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23155        // check the anchor and every peer term.
23156        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23157        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23158        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23159            return Err(self.err(String::from(
23160                "ORDER BY in a recursive query is not implemented",
23161            )));
23162        }
23163        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23164            return Err(self.err(String::from(
23165                "LIMIT in a recursive query is not implemented",
23166            )));
23167        }
23168        let self_refs = |s: &SelectStatement| -> usize {
23169            let Some(from) = &s.from else {
23170                return 0;
23171            };
23172            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23173            for j in &from.joins {
23174                if j.table.name.eq_ignore_ascii_case(&cte.name) {
23175                    n += 1;
23176                }
23177            }
23178            n
23179        };
23180        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23181            return Err(self.err(alloc::format!(
23182                "recursive reference to query \"{}\" must not appear more than once",
23183                cte.name
23184            )));
23185        }
23186        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23187        // apply only when the body actually references itself (a non-self-
23188        // referencing CTE under WITH RECURSIVE may use any set-op shape).
23189        let anchor_refs = self_refs(body);
23190        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23191        if anchor_refs > 0 || union_refs {
23192            // Shape: the top level must be UNION [ALL] arms only. A self-ref
23193            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23194            // "does not have the form" error — SPG used to compute a value.
23195            if body.unions.is_empty()
23196                || body.unions.iter().any(|(k, _)| {
23197                    !matches!(
23198                        k,
23199                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23200                    )
23201                })
23202            {
23203                return Err(self.err(alloc::format!(
23204                    "recursive query \"{}\" does not have the form non-recursive-term \
23205                     UNION [ALL] recursive-term",
23206                    cte.name
23207                )));
23208            }
23209            if anchor_refs > 0 {
23210                return Err(self.err(alloc::format!(
23211                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
23212                    cte.name
23213                )));
23214            }
23215        }
23216        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23217        for (_, u) in &body.unions {
23218            if self_refs(u) == 0 {
23219                continue;
23220            }
23221            // The self-reference must not sit on the nullable side of an outer
23222            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23223            if let Some(from) = &u.from {
23224                for (i, j) in from.joins.iter().enumerate() {
23225                    let left_has_self = is_self(&from.primary)
23226                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23227                    let violated = match j.kind {
23228                        crate::ast::JoinKind::Left => is_self(&j.table),
23229                        crate::ast::JoinKind::Right => left_has_self,
23230                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23231                        _ => false,
23232                    };
23233                    if violated {
23234                        return Err(self.err(alloc::format!(
23235                            "recursive reference to query \"{}\" must not appear within an outer join",
23236                            cte.name
23237                        )));
23238                    }
23239                }
23240            }
23241            // No aggregates at the top level of the recursive term (SPG used
23242            // to run them and surface a misleading downstream error).
23243            let mut items_and_having: Vec<&Expr> = Vec::new();
23244            for it in &u.items {
23245                if let crate::ast::SelectItem::Expr { expr, .. } = it {
23246                    items_and_having.push(expr);
23247                }
23248            }
23249            if let Some(h) = &u.having {
23250                items_and_having.push(h);
23251            }
23252            for e in items_and_having {
23253                if expr_has_toplevel_aggregate(e) {
23254                    return Err(self.err(String::from(
23255                        "aggregate functions are not allowed in a recursive query's recursive term",
23256                    )));
23257                }
23258            }
23259        }
23260        // A self-reference inside a sublink expression (EXISTS / IN / scalar
23261        // subquery) anywhere in the body is rejected; a plain FROM derived
23262        // table is legal in PG and untouched here.
23263        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23264        all_terms.extend(body.unions.iter().map(|(_, u)| u));
23265        for term in all_terms {
23266            if select_has_self_ref_in_sublink(term, &cte.name) {
23267                return Err(self.err(alloc::format!(
23268                    "recursive reference to query \"{}\" must not appear within a subquery",
23269                    cte.name
23270                )));
23271            }
23272        }
23273        Ok(())
23274    }
23275
23276    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23277    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23278    /// right after parse so the engine sees a plain recursive CTE with the
23279    /// tracking columns already projected. DEPTH FIRST and CYCLE are
23280    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23281    /// text-rendered rows can't provide, and errors honestly.
23282    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23283        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23284        if cte.search.is_none() && cte.cycle.is_none() {
23285            return Ok(());
23286        }
23287        let cte_name = cte.name.clone();
23288        let col_names = cte.column_overrides.clone();
23289        if col_names.is_empty() {
23290            return Err(
23291                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23292            );
23293        }
23294        let search = cte.search.take();
23295        let cycle = cte.cycle.take();
23296        let mut extra_cols: Vec<String> = Vec::new();
23297        let col_ref = |name: &str| {
23298            Expr::Column(ColumnName {
23299                qualifier: Some(cte_name.clone()),
23300                name: name.to_string(),
23301            })
23302        };
23303        // Position of a SEARCH/CYCLE column within the CTE's column list.
23304        let pos_of = |name: &str| -> Result<usize, ParseError> {
23305            col_names
23306                .iter()
23307                .position(|c| c.eq_ignore_ascii_case(name))
23308                .ok_or_else(|| {
23309                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23310                })
23311        };
23312        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23313            let mut args = Vec::with_capacity(positions.len());
23314            for &p in positions {
23315                match items.get(p) {
23316                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23317                    _ => {
23318                        return Err(self.err(
23319                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
23320                        ));
23321                    }
23322                }
23323            }
23324            Ok(Expr::FunctionCall {
23325                name: "row".into(),
23326                args,
23327            })
23328        };
23329        let CteBody::Select(body) = &mut cte.body else {
23330            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23331        };
23332        if body.unions.is_empty() {
23333            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23334        }
23335        let rec = body.unions.len() - 1; // recursive term = last UNION peer
23336
23337        if let Some(srch) = search {
23338            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23339            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23340            // no typed `record[]`, but element-wise array ORDER BY is correct
23341            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23342            // exactly onto a typed array: DEPTH is the root→node path
23343            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23344            // orders numerically (multi-digit keys included), matching PG.
23345            //
23346            // A multi-column BY would need a record[] to keep the per-node key
23347            // tuple orderable, which SPG can't express — error honestly there
23348            // rather than mis-order.
23349            if srch.by_columns.len() != 1 {
23350                return Err(self.err(
23351                    "SEARCH … BY with multiple columns needs typed record[] ordering \
23352                     SPG doesn't have yet; a single BY column is supported"
23353                        .into(),
23354                ));
23355            }
23356            let key_pos = pos_of(&srch.by_columns[0])?;
23357            let base_key = match body.items.get(key_pos) {
23358                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23359                _ => {
23360                    return Err(
23361                        self.err("SEARCH BY column maps to a non-expression select item".into())
23362                    );
23363                }
23364            };
23365            let rec_key = match body.unions[rec].1.items.get(key_pos) {
23366                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23367                _ => {
23368                    return Err(
23369                        self.err("SEARCH BY column maps to a non-expression select item".into())
23370                    );
23371                }
23372            };
23373            if srch.depth_first {
23374                // base: ARRAY[key]; rec: array_append(cte.set, key).
23375                body.items.push(SelectItem::Expr {
23376                    expr: Expr::Array(alloc::vec![base_key]),
23377                    alias: Some(srch.set_column.clone()),
23378                });
23379                body.unions[rec].1.items.push(SelectItem::Expr {
23380                    expr: Expr::FunctionCall {
23381                        name: "array_append".into(),
23382                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
23383                    },
23384                    alias: Some(srch.set_column.clone()),
23385                });
23386            } else {
23387                // BREADTH: [depth, key]; depth starts at 0 and increments. The
23388                // leading depth element dominates the element-wise comparison,
23389                // so shallower rows sort first, then by key — PG's (depth, key).
23390                body.items.push(SelectItem::Expr {
23391                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23392                    alias: Some(srch.set_column.clone()),
23393                });
23394                // rec depth = cte.set[1] + 1.
23395                let parent_depth = Expr::ArraySubscript {
23396                    target: Box::new(col_ref(&srch.set_column)),
23397                    index: Box::new(Expr::Literal(Literal::Integer(1))),
23398                };
23399                body.unions[rec].1.items.push(SelectItem::Expr {
23400                    expr: Expr::Array(alloc::vec![
23401                        Expr::Binary {
23402                            lhs: Box::new(parent_depth),
23403                            op: BinOp::Add,
23404                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23405                        },
23406                        rec_key,
23407                    ]),
23408                    alias: Some(srch.set_column.clone()),
23409                });
23410            }
23411            extra_cols.push(srch.set_column);
23412        }
23413
23414        if let Some(cyc) = cycle {
23415            let positions: Vec<usize> = cyc
23416                .columns
23417                .iter()
23418                .map(|c| pos_of(c))
23419                .collect::<Result<_, _>>()?;
23420            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23421            // cast it to text for the cycle path: membership only needs equality,
23422            // and the record text form gives SPG a TextArray path (SPG has no
23423            // typed record[] array). Cycle detection is unaffected.
23424            let base_row = Expr::Cast {
23425                expr: Box::new(row_of(&body.items, &positions)?),
23426                target: CastTarget::Text,
23427            };
23428            let rec_row = Expr::Cast {
23429                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23430                target: CastTarget::Text,
23431            };
23432            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23433            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23434            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23435            body.items.push(SelectItem::Expr {
23436                expr: Expr::Literal(dflt.clone()),
23437                alias: Some(cyc.mark_column.clone()),
23438            });
23439            body.items.push(SelectItem::Expr {
23440                expr: Expr::Array(alloc::vec![base_row]),
23441                alias: Some(cyc.path_column.clone()),
23442            });
23443            // rec mark: ROW(cols) already in the path → cycle.
23444            let hit = Expr::AnyAll {
23445                expr: Box::new(rec_row.clone()),
23446                op: BinOp::Eq,
23447                array: Box::new(col_ref(&cyc.path_column)),
23448                is_any: true,
23449            };
23450            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23451                Expr::Case {
23452                    operand: None,
23453                    branches: alloc::vec![(hit, Expr::Literal(mark))],
23454                    else_branch: Some(Box::new(Expr::Literal(dflt))),
23455                }
23456            } else {
23457                hit
23458            };
23459            body.unions[rec].1.items.push(SelectItem::Expr {
23460                expr: mark_expr,
23461                alias: Some(cyc.mark_column.clone()),
23462            });
23463            // rec path: array_append(cte.path, ROW(cols)).
23464            body.unions[rec].1.items.push(SelectItem::Expr {
23465                expr: Expr::FunctionCall {
23466                    name: "array_append".into(),
23467                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23468                },
23469                alias: Some(cyc.path_column.clone()),
23470            });
23471            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23472            let stop = Expr::Unary {
23473                op: UnOp::Not,
23474                expr: Box::new(col_ref(&cyc.mark_column)),
23475            };
23476            let w = &mut body.unions[rec].1.where_;
23477            *w = Some(match w.take() {
23478                Some(prev) => Expr::Binary {
23479                    lhs: Box::new(prev),
23480                    op: BinOp::And,
23481                    rhs: Box::new(stop),
23482                },
23483                None => stop,
23484            });
23485            extra_cols.push(cyc.mark_column);
23486            extra_cols.push(cyc.path_column);
23487        }
23488        cte.column_overrides.extend(extra_cols);
23489        Ok(())
23490    }
23491
23492    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23493    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23494    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23495        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23496            return Ok(None);
23497        }
23498        self.advance(); // SEARCH
23499        let depth_first = match self.peek() {
23500            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23501            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23502            other => {
23503                return Err(self.err(format!(
23504                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23505                )));
23506            }
23507        };
23508        self.advance();
23509        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23510            return Err(self.err(format!(
23511                "expected FIRST after SEARCH mode, got {:?}",
23512                self.peek()
23513            )));
23514        }
23515        self.advance();
23516        if !self.peek_is_by() {
23517            return Err(self.err(format!(
23518                "expected BY after SEARCH … FIRST, got {:?}",
23519                self.peek()
23520            )));
23521        }
23522        self.advance();
23523        let mut by_columns = alloc::vec![self.expect_ident_like()?];
23524        while matches!(self.peek(), Token::Comma) {
23525            self.advance();
23526            by_columns.push(self.expect_ident_like()?);
23527        }
23528        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23529            return Err(self.err(format!(
23530                "expected SET in SEARCH clause, got {:?}",
23531                self.peek()
23532            )));
23533        }
23534        self.advance();
23535        let set_column = self.expect_ident_like()?;
23536        Ok(Some(crate::ast::SearchClause {
23537            depth_first,
23538            by_columns,
23539            set_column,
23540        }))
23541    }
23542
23543    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23544    /// USING pathcol`. Returns None when the next token isn't CYCLE.
23545    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23546        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23547            return Ok(None);
23548        }
23549        self.advance(); // CYCLE
23550        let mut columns = alloc::vec![self.expect_ident_like()?];
23551        while matches!(self.peek(), Token::Comma) {
23552            self.advance();
23553            columns.push(self.expect_ident_like()?);
23554        }
23555        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23556            return Err(self.err(format!(
23557                "expected SET in CYCLE clause, got {:?}",
23558                self.peek()
23559            )));
23560        }
23561        self.advance();
23562        let mark_column = self.expect_ident_like()?;
23563        let mut mark_value = None;
23564        let mut default_value = None;
23565        if matches!(self.peek(), Token::To) {
23566            self.advance();
23567            mark_value = Some(self.parse_cycle_literal()?);
23568            if !matches!(self.peek(), Token::Default) {
23569                return Err(self.err(format!(
23570                    "expected DEFAULT after CYCLE … TO, got {:?}",
23571                    self.peek()
23572                )));
23573            }
23574            self.advance();
23575            default_value = Some(self.parse_cycle_literal()?);
23576        }
23577        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23578            return Err(self.err(format!(
23579                "expected USING in CYCLE clause, got {:?}",
23580                self.peek()
23581            )));
23582        }
23583        self.advance();
23584        let path_column = self.expect_ident_like()?;
23585        Ok(Some(crate::ast::CycleClause {
23586            columns,
23587            mark_column,
23588            mark_value,
23589            default_value,
23590            path_column,
23591        }))
23592    }
23593
23594    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23595    /// literal (string / bool / number) in PG.
23596    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23597        match self.parse_expr(0)? {
23598            Expr::Literal(l) => Ok(l),
23599            other => Err(self.err(format!(
23600                "CYCLE mark/default value must be a literal, got {other:?}"
23601            ))),
23602        }
23603    }
23604
23605    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23606        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23607        // Comes through as an identifier; consume it if present and
23608        // mark every CTE in the clause as recursive (PG semantics —
23609        // the flag is per-WITH, not per-CTE).
23610        let mut recursive = false;
23611        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23612            && s.eq_ignore_ascii_case("recursive")
23613        {
23614            self.advance();
23615            recursive = true;
23616        }
23617        let mut ctes = Vec::new();
23618        loop {
23619            let name = self.expect_ident_like()?;
23620            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23621            // PG uses these to rename the body's output columns; we
23622            // do the same below by overriding `columns[i].name`.
23623            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23624                self.advance();
23625                let mut names = Vec::new();
23626                loop {
23627                    names.push(self.expect_ident_like()?);
23628                    if matches!(self.peek(), Token::Comma) {
23629                        self.advance();
23630                        continue;
23631                    }
23632                    break;
23633                }
23634                if !matches!(self.peek(), Token::RParen) {
23635                    return Err(self.err(format!(
23636                        "expected ')' to close CTE column list, got {:?}",
23637                        self.peek()
23638                    )));
23639                }
23640                self.advance();
23641                names
23642            } else {
23643                Vec::new()
23644            };
23645            // AS is a reserved Token::As (used by SELECT-item / FROM
23646            // aliasing) — handle it specially rather than as a bare
23647            // ident.
23648            if !matches!(self.peek(), Token::As) {
23649                return Err(self.err(format!(
23650                    "expected AS after CTE name {name:?}, got {:?}",
23651                    self.peek()
23652                )));
23653            }
23654            self.advance();
23655            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23656            // MATERIALIZED` optimizer hints. SPG materialises every
23657            // CTE, so both spellings are accepted and absorbed.
23658            if matches!(self.peek(), Token::Not) {
23659                self.advance(); // NOT
23660                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23661                    if s.eq_ignore_ascii_case("materialized"))
23662                {
23663                    self.advance();
23664                } else {
23665                    return Err(self.err(format!(
23666                        "expected MATERIALIZED after AS NOT, got {:?}",
23667                        self.peek()
23668                    )));
23669                }
23670            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23671                if s.eq_ignore_ascii_case("materialized"))
23672            {
23673                self.advance();
23674            }
23675            if !matches!(self.peek(), Token::LParen) {
23676                return Err(self.err(format!(
23677                    "expected '(' after AS in WITH clause, got {:?}",
23678                    self.peek()
23679                )));
23680            }
23681            self.advance();
23682            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23683            // RETURNING) as the CTE body in addition to SELECT.
23684            // PG writable CTE semantics. UPDATE / DELETE come in as
23685            // bare Idents (lexer keeps SELECT / INSERT as reserved
23686            // tokens but treats the rest of DML as case-insensitive
23687            // idents).
23688            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23689            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23690            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23691            let body = match self.peek() {
23692                Token::Select => {
23693                    let inner = self.parse_select_stmt()?;
23694                    let Statement::Select(s) = inner else {
23695                        unreachable!("parse_select_stmt returns Select");
23696                    };
23697                    crate::ast::CteBody::Select(s)
23698                }
23699                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23700                // `SELECT * FROM t` this way and accepts it wherever a
23701                // SELECT goes, so the CTE body dispatch needs its own
23702                // arm: this match is keyed on the FIRST token, and
23703                // `Token::Table` fell through to a tail that then
23704                // rejected what it got. `parse_table_shorthand` has
23705                // returned a desugared SelectStatement since the
23706                // shorthand landed — only the routing was missing.
23707                // Round 868 found this by putting the shorthand in a
23708                // subquery; every earlier check used a top-level form.
23709                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23710                // `SELECT * FROM t` this way and accepts it wherever a
23711                // SELECT goes, so the CTE body dispatch needs its own
23712                // arm: this match is keyed on the FIRST token, and
23713                // `Token::Table` fell through to a tail that rejected
23714                // what it got. `parse_table_shorthand` has returned a
23715                // desugared SelectStatement since the shorthand landed —
23716                // only the routing was missing, here and in the derived
23717                // table's second-token gate. Round 868 found both by
23718                // putting the shorthand in a subquery; every earlier
23719                // check had used a top-level form.
23720                Token::Table
23721                    if matches!(
23722                        self.tokens.get(self.pos + 1),
23723                        Some(Token::Ident(_) | Token::QuotedIdent(_))
23724                    ) =>
23725                {
23726                    let mut head = self.parse_table_shorthand()?;
23727                    self.parse_setop_chain_into(&mut head)?;
23728                    self.parse_select_tail_into(&mut head)?;
23729                    crate::ast::CteBody::Select(head)
23730                }
23731                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23732                // WITH t(a) AS (VALUES (1), (2)) … lowers through
23733                // the shared rows helper onto a Select body.
23734                Token::Values => {
23735                    self.advance(); // VALUES
23736                    let mut head = self.parse_values_rows_body()?;
23737                    // A VALUES seed can head a set-operation chain —
23738                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23739                    // SELECT n+1 FROM t …). Attach any trailing
23740                    // UNION / INTERSECT / EXCEPT peers so the
23741                    // recursive-CTE body parses like the SELECT seed.
23742                    self.parse_setop_chain_into(&mut head)?;
23743                    crate::ast::CteBody::Select(head)
23744                }
23745                Token::Insert => {
23746                    let inner = self.parse_one_statement()?;
23747                    let Statement::Insert(s) = inner else {
23748                        unreachable!("Token::Insert routes to Insert");
23749                    };
23750                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23751                }
23752                _ if is_update_kw => {
23753                    let inner = self.parse_one_statement()?;
23754                    let Statement::Update(s) = inner else {
23755                        return Err(
23756                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23757                        );
23758                    };
23759                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23760                }
23761                _ if is_delete_kw => {
23762                    let inner = self.parse_one_statement()?;
23763                    let Statement::Delete(s) = inner else {
23764                        return Err(
23765                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23766                        );
23767                    };
23768                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23769                }
23770                // v7.39 (round 149) — PG 17 allows MERGE as a
23771                // data-modifying CTE body.
23772                _ if is_merge_kw => {
23773                    let inner = self.parse_one_statement()?;
23774                    let Statement::Merge(s) = inner else {
23775                        return Err(
23776                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23777                        );
23778                    };
23779                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23780                }
23781                // v7.39 (round 151) — a CTE body may itself be
23782                // WITH-headed (PG grammar: PreparableStmt carries its
23783                // own with_clause). The nested statement keeps its own
23784                // ctes; the modifying-CTE-at-top-level rule is enforced
23785                // at execution.
23786                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
23787                    self.advance(); // WITH
23788                    match self.parse_with_cte_then_select()? {
23789                        Statement::Select(s) => crate::ast::CteBody::Select(s),
23790                        Statement::Insert(s) => {
23791                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23792                        }
23793                        Statement::Update(s) => {
23794                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23795                        }
23796                        Statement::Delete(s) => {
23797                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23798                        }
23799                        Statement::Merge(s) => {
23800                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23801                        }
23802
23803                        other => {
23804                            return Err(self.err(format!(
23805                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23806                            )));
23807                        }
23808                    }
23809                }
23810                other => {
23811                    return Err(self.err(format!(
23812                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23813                    )));
23814                }
23815            };
23816            if !matches!(self.peek(), Token::RParen) {
23817                return Err(self.err(format!(
23818                    "expected ')' after CTE body, got {:?}",
23819                    self.peek()
23820                )));
23821            }
23822            self.advance();
23823            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
23824            // CTE, desugared into extra body columns by the engine.
23825            let search = self.parse_cte_search_clause()?;
23826            let cycle = self.parse_cte_cycle_clause()?;
23827            let mut cte = crate::ast::Cte {
23828                name,
23829                body,
23830                recursive,
23831                column_overrides,
23832                search,
23833                cycle,
23834            };
23835            self.validate_recursive_cte(&cte)?;
23836            self.desugar_cte_search_cycle(&mut cte)?;
23837            ctes.push(cte);
23838            if matches!(self.peek(), Token::Comma) {
23839                self.advance();
23840                continue;
23841            }
23842            break;
23843        }
23844        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
23845        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
23846        // the parsed CTEs to whichever statement the body produces.
23847        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23848        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23849        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23850        match self.peek() {
23851            Token::Select => {
23852                let body_stmt = self.parse_select_stmt()?;
23853                let Statement::Select(mut body) = body_stmt else {
23854                    unreachable!()
23855                };
23856                body.ctes = ctes;
23857                Ok(Statement::Select(body))
23858            }
23859            Token::Insert => {
23860                let body_stmt = self.parse_one_statement()?;
23861                let Statement::Insert(mut body) = body_stmt else {
23862                    unreachable!()
23863                };
23864                body.ctes = ctes;
23865                Ok(Statement::Insert(body))
23866            }
23867            _ if outer_is_update => {
23868                let body_stmt = self.parse_one_statement()?;
23869                let Statement::Update(mut body) = body_stmt else {
23870                    return Err(self.err(format!("expected UPDATE after WITH clause")));
23871                };
23872                body.ctes = ctes;
23873                Ok(Statement::Update(body))
23874            }
23875            _ if outer_is_delete => {
23876                let body_stmt = self.parse_one_statement()?;
23877                let Statement::Delete(mut body) = body_stmt else {
23878                    return Err(self.err(format!("expected DELETE after WITH clause")));
23879                };
23880                body.ctes = ctes;
23881                Ok(Statement::Delete(body))
23882            }
23883            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
23884            // WITH RECURSIVE is rejected with PG's exact message
23885            // (parse analysis, transformWithClause).
23886            _ if outer_is_merge => {
23887                if recursive {
23888                    return Err(self.err(String::from(
23889                        "WITH RECURSIVE is not supported for MERGE statement",
23890                    )));
23891                }
23892                let body_stmt = self.parse_one_statement()?;
23893                let Statement::Merge(mut body) = body_stmt else {
23894                    return Err(self.err(format!("expected MERGE after WITH clause")));
23895                };
23896                body.ctes = ctes;
23897                Ok(Statement::Merge(body))
23898            }
23899            other => Err(self.err(format!(
23900                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
23901            ))),
23902        }
23903    }
23904
23905    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
23906    /// already consumed the leading `EXISTS` ident via
23907    /// `self.advance()`.
23908    /// v7.13.0 — parse the rest of a `CASE … END` expression after
23909    /// the leading `CASE` ident has been consumed (mailrs round-5
23910    /// G9). Supports both the searched form
23911    /// (`CASE WHEN cond THEN val …`) and the simple form
23912    /// (`CASE operand WHEN val THEN val …`).
23913    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
23914        // Disambiguate searched vs simple form: if the next token
23915        // is `WHEN`, we're in the searched form. Otherwise the
23916        // intervening expression is the operand.
23917        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
23918            None
23919        } else {
23920            Some(Box::new(self.parse_expr(0)?))
23921        };
23922        let mut branches: Vec<(Expr, Expr)> = Vec::new();
23923        loop {
23924            match self.peek() {
23925                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
23926                    self.advance();
23927                    let cond = self.parse_expr(0)?;
23928                    match self.peek() {
23929                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
23930                            self.advance();
23931                        }
23932                        other => {
23933                            return Err(self.err(alloc::format!(
23934                                "expected THEN after CASE WHEN <expr>, got {other:?}"
23935                            )));
23936                        }
23937                    }
23938                    let value = self.parse_expr(0)?;
23939                    branches.push((cond, value));
23940                }
23941                _ => break,
23942            }
23943        }
23944        if branches.is_empty() {
23945            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
23946        }
23947        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
23948        {
23949            self.advance();
23950            Some(Box::new(self.parse_expr(0)?))
23951        } else {
23952            None
23953        };
23954        match self.peek() {
23955            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
23956                self.advance();
23957            }
23958            other => {
23959                return Err(self.err(alloc::format!(
23960                    "expected END to close CASE expression, got {other:?}"
23961                )));
23962            }
23963        }
23964        Ok(Expr::Case {
23965            operand,
23966            branches,
23967            else_branch,
23968        })
23969    }
23970
23971    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
23972    /// query-source position (EXISTS / IN / INSERT source / CTE body /
23973    /// view body). Caller consumed the WITH keyword. Only a SELECT
23974    /// outer is grammatical here; the data-modifying-CTE-at-top-level
23975    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
23976    /// maps correctly.
23977    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23978        let inner = self.parse_with_cte_then_select()?;
23979        match inner {
23980            Statement::Select(s) => Ok(s),
23981            other => Err(self.err(format!(
23982                "expected SELECT after WITH in a subquery, got {other:?}"
23983            ))),
23984        }
23985    }
23986
23987    /// True when the next token is the (unquoted) WITH keyword. WITH is
23988    /// reserved in PG, so a bare `with` can never be a column reference
23989    /// in these positions; a quoted `"with"` stays an identifier.
23990    fn peek_is_with_kw(&self) -> bool {
23991        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
23992    }
23993
23994    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
23995    /// `#[inline(never)]` keeps the large SelectStatement temporaries
23996    /// off parse_expr's recursive frame (the nesting-budget stack
23997    /// cliff — see the round-153 gate regression).
23998    #[inline(never)]
23999    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24000        if self.peek_is_with_kw() {
24001            self.advance();
24002            self.parse_nested_with_select()
24003        } else {
24004            match self.parse_select_stmt()? {
24005                Statement::Select(s) => Ok(s),
24006                other => Err(self.err(alloc::format!(
24007                    "expected SELECT inside ANY/ALL, got {other:?}"
24008                ))),
24009            }
24010        }
24011    }
24012
24013    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
24014        if !matches!(self.peek(), Token::LParen) {
24015            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
24016        }
24017        self.advance();
24018        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
24019        let s = if self.peek_is_with_kw() {
24020            self.advance();
24021            self.parse_nested_with_select()?
24022        } else {
24023            let inner = self.parse_select_stmt()?;
24024            let Statement::Select(s) = inner else {
24025                unreachable!("parse_select_stmt returns Select")
24026            };
24027            s
24028        };
24029        if !matches!(self.peek(), Token::RParen) {
24030            return Err(self.err(format!(
24031                "expected ')' after EXISTS-subquery, got {:?}",
24032                self.peek()
24033            )));
24034        }
24035        self.advance();
24036        Ok(Expr::Exists {
24037            subquery: Box::new(s),
24038            negated,
24039        })
24040    }
24041
24042    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24043        self.advance(); // IN
24044        if !matches!(self.peek(), Token::LParen) {
24045            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
24046        }
24047        self.advance();
24048        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
24049        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
24050        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
24051            let s = if self.peek_is_with_kw() {
24052                self.advance();
24053                self.parse_nested_with_select()?
24054            } else {
24055                let inner = self.parse_select_stmt()?;
24056                let Statement::Select(s) = inner else {
24057                    unreachable!("parse_select_stmt always returns Statement::Select")
24058                };
24059                s
24060            };
24061            if !matches!(self.peek(), Token::RParen) {
24062                return Err(self.err(format!(
24063                    "expected ')' after IN-subquery, got {:?}",
24064                    self.peek()
24065                )));
24066            }
24067            self.advance();
24068            return Ok(Expr::InSubquery {
24069                expr: Box::new(expr),
24070                subquery: Box::new(s),
24071                negated,
24072            });
24073        }
24074        let mut elements = Vec::new();
24075        if !matches!(self.peek(), Token::RParen) {
24076            loop {
24077                elements.push(self.parse_expr(0)?);
24078                match self.peek() {
24079                    Token::Comma => {
24080                        self.advance();
24081                    }
24082                    Token::RParen => break,
24083                    other => {
24084                        return Err(
24085                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24086                        );
24087                    }
24088                }
24089            }
24090        }
24091        self.advance(); // ')'
24092        // v7.30.2 (mailrs round-25) — flat InList node instead of a
24093        // left-deep OR-Eq chain: chain depth scaled with the element
24094        // count and overflowed the stack (eval + drop are recursive).
24095        if elements.is_empty() {
24096            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24097        }
24098        Ok(Expr::InList {
24099            expr: Box::new(expr),
24100            list: elements,
24101            negated,
24102        })
24103    }
24104
24105    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24106    /// already consumed by the caller. Elements must be numeric literals
24107    /// (with optional unary `-`); any compound expression is rejected at
24108    /// parse time so the runtime never needs to evaluate inside a vector.
24109    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24110    /// has already consumed the `EXTRACT` token before calling us —
24111    /// we pick up at the opening `(`.
24112    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24113    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24114    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24115    /// per-column OR-fold of
24116    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24117    /// term)` so the existing FTS evaluator handles semantics.
24118    ///
24119    /// The mode modifier is accepted-and-ignored at v7.17 — all
24120    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24121    /// mode operators (`+foo -bar`) would need their own parser
24122    /// (Phase 2.2c); customers who hit them today already get a
24123    /// correct lexeme-match against the bare term, only without
24124    /// the +/- precedence the customer asked for.
24125    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24126        // Already at `MATCH`-consumed position; the dispatcher
24127        // confirmed the next token is `(`.
24128        if !matches!(self.peek(), Token::LParen) {
24129            return Err(self.err(alloc::format!(
24130                "expected '(' after MATCH, got {:?}",
24131                self.peek()
24132            )));
24133        }
24134        self.advance();
24135        let mut cols: Vec<Expr> = Vec::new();
24136        loop {
24137            cols.push(self.parse_expr(0)?);
24138            match self.peek() {
24139                Token::Comma => {
24140                    self.advance();
24141                }
24142                Token::RParen => break,
24143                other => {
24144                    return Err(self.err(alloc::format!(
24145                        "expected ',' or ')' in MATCH column list, got {other:?}"
24146                    )));
24147                }
24148            }
24149        }
24150        self.advance(); // ')'
24151        // Expect AGAINST.
24152        match self.peek() {
24153            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24154                self.advance();
24155            }
24156            other => {
24157                return Err(self.err(alloc::format!(
24158                    "expected AGAINST after MATCH column list, got {other:?}"
24159                )));
24160            }
24161        }
24162        if !matches!(self.peek(), Token::LParen) {
24163            return Err(self.err(alloc::format!(
24164                "expected '(' after AGAINST, got {:?}",
24165                self.peek()
24166            )));
24167        }
24168        self.advance();
24169        // Read AGAINST's argument as a single primary token —
24170        // string literal, placeholder, or column-ref ident. We
24171        // can't call `parse_expr` / `parse_unary` here because
24172        // the postfix chain inside `parse_atom` would greedily
24173        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24174        // and fail at "expected '(' after IN". Customers always
24175        // write a literal or bound parameter in AGAINST, so this
24176        // restriction is non-blocking; the error path explains
24177        // the limit if a more complex expression shows up.
24178        let term = match self.advance() {
24179            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24180            Token::Placeholder(n) => Expr::Placeholder(n),
24181            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24182                qualifier: None,
24183                name: s,
24184            }),
24185            other => {
24186                return Err(self.err(alloc::format!(
24187                    "MATCH ... AGAINST(<term>) expects a string literal, \
24188                     bound parameter, or column ref, got {other:?}"
24189                )));
24190            }
24191        };
24192        // Optional mode tail — accept-and-ignore at v7.17:
24193        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24194        //   IN BOOLEAN MODE
24195        //   WITH QUERY EXPANSION
24196        loop {
24197            match self.peek() {
24198                // IN lexes as a reserved Token::In, not an ident,
24199                // so it gets its own arm.
24200                Token::In => {
24201                    self.advance();
24202                }
24203                Token::Ident(s) | Token::QuotedIdent(s)
24204                    if s.eq_ignore_ascii_case("natural")
24205                        || s.eq_ignore_ascii_case("language")
24206                        || s.eq_ignore_ascii_case("boolean")
24207                        || s.eq_ignore_ascii_case("mode")
24208                        || s.eq_ignore_ascii_case("with")
24209                        || s.eq_ignore_ascii_case("query")
24210                        || s.eq_ignore_ascii_case("expansion") =>
24211                {
24212                    self.advance();
24213                }
24214                _ => break,
24215            }
24216        }
24217        if !matches!(self.peek(), Token::RParen) {
24218            return Err(self.err(alloc::format!(
24219                "expected ')' to close AGAINST, got {:?}",
24220                self.peek()
24221            )));
24222        }
24223        self.advance();
24224        // Build per-column `to_tsvector('simple', col) @@
24225        // plainto_tsquery('simple', term)` and OR-fold.
24226        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24227        let plainto = Expr::FunctionCall {
24228            name: String::from("plainto_tsquery"),
24229            args: alloc::vec![simple_lit(), term.clone()],
24230        };
24231        let mut folded: Option<Expr> = None;
24232        for col in cols {
24233            let to_tsv = Expr::FunctionCall {
24234                name: String::from("to_tsvector"),
24235                args: alloc::vec![simple_lit(), col],
24236            };
24237            let leaf = Expr::Binary {
24238                lhs: Box::new(to_tsv),
24239                op: crate::ast::BinOp::TsMatch,
24240                rhs: Box::new(plainto.clone()),
24241            };
24242            folded = Some(match folded {
24243                None => leaf,
24244                Some(prev) => Expr::Binary {
24245                    lhs: Box::new(prev),
24246                    op: crate::ast::BinOp::Or,
24247                    rhs: Box::new(leaf),
24248                },
24249            });
24250        }
24251        match folded {
24252            Some(e) => Ok(e),
24253            None => Err(self.err(String::from(
24254                "MATCH(...) AGAINST(...) requires at least one column",
24255            ))),
24256        }
24257    }
24258
24259    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24260        if !matches!(self.peek(), Token::LParen) {
24261            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24262        }
24263        self.advance();
24264        let field_name = self.expect_ident_like()?;
24265        let field = match field_name.to_ascii_lowercase().as_str() {
24266            // PG accepts the plural spellings (years/months/…/millenniums) as
24267            // aliases for the singular fields — its datetime unit table has both.
24268            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24269            "year" | "years" => ExtractField::Year,
24270            "month" | "months" => ExtractField::Month,
24271            "day" | "days" => ExtractField::Day,
24272            "hour" | "hours" => ExtractField::Hour,
24273            "minute" | "minutes" => ExtractField::Minute,
24274            "second" | "seconds" => ExtractField::Second,
24275            "microsecond" | "microseconds" => ExtractField::Microsecond,
24276            "epoch" => ExtractField::Epoch,
24277            "dow" => ExtractField::Dow,
24278            "isodow" => ExtractField::Isodow,
24279            "doy" => ExtractField::Doy,
24280            "week" | "weeks" => ExtractField::Week,
24281            "isoyear" => ExtractField::Isoyear,
24282            "quarter" => ExtractField::Quarter,
24283            "decade" | "decades" => ExtractField::Decade,
24284            "century" | "centuries" => ExtractField::Century,
24285            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24286            "julian" => ExtractField::Julian,
24287            "millisecond" | "milliseconds" => ExtractField::Millisecond,
24288            "timezone" => ExtractField::Timezone,
24289            "timezone_hour" => ExtractField::TimezoneHour,
24290            "timezone_minute" => ExtractField::TimezoneMinute,
24291            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24292            // reports an unknown one with the source type (22023); carry the
24293            // raw name so eval can word it.
24294            other => ExtractField::Other(alloc::string::String::from(other)),
24295        };
24296        if !matches!(self.peek(), Token::From) {
24297            return Err(self.err(format!(
24298                "expected FROM after EXTRACT field, got {:?}",
24299                self.peek()
24300            )));
24301        }
24302        self.advance();
24303        let source = self.parse_expr(0)?;
24304        if !matches!(self.peek(), Token::RParen) {
24305            return Err(self.err(format!(
24306                "expected ')' to close EXTRACT, got {:?}",
24307                self.peek()
24308            )));
24309        }
24310        self.advance();
24311        Ok(Expr::Extract {
24312            field,
24313            source: Box::new(source),
24314        })
24315    }
24316
24317    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24318    /// is already consumed; we expect a single string literal next and
24319    /// resolve it into `Literal::Interval` at parse time so the engine
24320    /// never has to re-tokenise inside the string.
24321    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24322    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24323    /// is the SQL-standard form and is left to the path below.
24324    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24325        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24326        let (offset, sign) = match self.peek() {
24327            Token::Minus => (1, "-"),
24328            _ => (0, ""),
24329        };
24330        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24331            return None;
24332        };
24333        self.tokens
24334            .get(self.pos + offset + 1)
24335            .filter(|t| mysql_interval_unit(t).is_some())?;
24336        Some((alloc::format!("{sign}{n}"), offset + 1))
24337    }
24338
24339    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24340    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24341    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24342    ///
24343    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24344    /// this by parsing the group and then restoring `self.pos` — which could
24345    /// never have worked, because `advance()` DESTROYS the token it returns
24346    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24347    /// inert only because both branches errored back then.
24348    fn interval_paren_is_quantity(&self) -> bool {
24349        let mut depth = 0usize;
24350        let mut saw_top_level_comma = false;
24351        let mut i = self.pos;
24352        while let Some(tok) = self.tokens.get(i) {
24353            match tok {
24354                Token::LParen => depth += 1,
24355                Token::RParen => {
24356                    depth = depth.saturating_sub(1);
24357                    if depth == 0 {
24358                        return !saw_top_level_comma
24359                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24360                                .is_some();
24361                    }
24362                }
24363                // A comma directly inside the outermost parens means the
24364                // argument list of the INTERVAL() function.
24365                Token::Comma if depth == 1 => saw_top_level_comma = true,
24366                Token::Eof => return false,
24367                _ => {}
24368            }
24369            i += 1;
24370        }
24371        false
24372    }
24373
24374    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24375        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24376        // (the index of the last Ni ≤ N), distinct from the interval literal.
24377        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24378        // is decided by a non-destructive lookahead (round 422) before either
24379        // branch consumes anything. MySQL only.
24380        if self.mysql_dialect
24381            && matches!(self.peek(), Token::LParen)
24382            && !self.interval_paren_is_quantity()
24383        {
24384            self.advance(); // (
24385            let mut args = Vec::new();
24386            if !matches!(self.peek(), Token::RParen) {
24387                loop {
24388                    args.push(self.parse_expr(0)?);
24389                    if matches!(self.peek(), Token::Comma) {
24390                        self.advance();
24391                        continue;
24392                    }
24393                    break;
24394                }
24395            }
24396            if !matches!(self.peek(), Token::RParen) {
24397                return Err(self.err(alloc::format!(
24398                    "expected ')' after INTERVAL() arguments, got {:?}",
24399                    self.peek()
24400                )));
24401            }
24402            self.advance(); // )
24403            return Ok(Expr::FunctionCall {
24404                name: alloc::string::String::from("interval"),
24405                args,
24406            });
24407        }
24408        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24409        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24410        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24411        // writes every date arithmetic there is, and it did not parse at
24412        // all. PG rejects the unquoted form outright (`syntax error at or
24413        // near "1"`, measured), so it is taken only in the MySQL dialect —
24414        // PG's own `INTERVAL '1' DAY` is untouched below.
24415        if self.mysql_dialect
24416            && let Some((text, consume)) = self.peek_unquoted_interval_count()
24417        {
24418            for _ in 0..consume {
24419                self.advance(); // the optional `-` and the number
24420            }
24421            let Some(unit) = mysql_interval_unit(self.peek()) else {
24422                return Err(self.err(alloc::format!(
24423                    "expected an interval unit after INTERVAL {text}, got {:?}",
24424                    self.peek()
24425                )));
24426            };
24427            self.advance(); // the unit
24428            let (months, days, micros) = scale_mysql_interval(&text, unit)
24429                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24430            return Ok(Expr::Literal(Literal::Interval {
24431                months,
24432                days,
24433                micros,
24434                // The canonical rendering, so Display round-trips into a
24435                // form both dialects read back.
24436                text: alloc::format!("{text} {unit}"),
24437            }));
24438        }
24439        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24440        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24441        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24442        // Those cannot fold into a compile-time `Literal::Interval`, so they
24443        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24444        // builtin, which builds the value at run time (and yields NULL for a
24445        // NULL quantity, as MariaDB does). The literal path above still folds
24446        // the constant case — it is cheaper and round-trips through Display.
24447        //
24448        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24449        // MySQL's quoted spelling) keep the qualifier path below.
24450        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24451            let qty = self.parse_expr(0)?;
24452            let Some(unit) = mysql_interval_unit(self.peek()) else {
24453                return Err(self.err(alloc::format!(
24454                    "expected an interval unit after INTERVAL <expr>, got {:?}",
24455                    self.peek()
24456                )));
24457            };
24458            self.advance(); // the unit
24459            return Ok(make_interval_call(qty, unit));
24460        }
24461        let tok = self.advance();
24462        let Token::String(text) = tok else {
24463            return Err(self.err(format!(
24464                "expected string literal after INTERVAL, got {tok:?}"
24465            )));
24466        };
24467        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24468        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24469        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24470        // bare number means and the leading/trailing precision.
24471        let field1 = interval_field_of(self.peek());
24472        let qualifier = if let Some(f1) = field1 {
24473            self.advance();
24474            let f2 = if matches!(self.peek(), Token::To) {
24475                self.advance();
24476                let Some(f) = interval_field_of(self.peek()) else {
24477                    return Err(self.err(format!(
24478                        "expected an interval field after TO, got {:?}",
24479                        self.peek()
24480                    )));
24481                };
24482                self.advance();
24483                Some(f)
24484            } else {
24485                None
24486            };
24487            Some((f1, f2))
24488        } else {
24489            None
24490        };
24491        let (months, days, micros) = match qualifier {
24492            Some(q) => interpret_qualified_interval(&text, q),
24493            None => parse_interval_text(&text),
24494        }
24495        .ok_or_else(|| ParseError {
24496            message: format!(
24497                "cannot parse INTERVAL {text:?}; \
24498                     expected `<n> <unit> [<n> <unit> ...]` with units \
24499                     microsecond[s], millisecond[s], second[s], minute[s], \
24500                     hour[s], day[s], week[s], month[s], year[s]"
24501            ),
24502            token_pos: self.consumed_pos(),
24503        })?;
24504        Ok(Expr::Literal(Literal::Interval {
24505            months,
24506            days,
24507            micros,
24508            text,
24509        }))
24510    }
24511
24512    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24513    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24514    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24515    /// than a pgvector literal.
24516    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24517        self.advance(); // consume `[`
24518        let mut items: Vec<Expr> = Vec::new();
24519        if !matches!(self.peek(), Token::RBracket) {
24520            loop {
24521                if matches!(self.peek(), Token::LBracket) {
24522                    items.push(self.parse_array_bracket_body()?);
24523                } else {
24524                    items.push(self.parse_expr(0)?);
24525                }
24526                match self.peek() {
24527                    Token::Comma => {
24528                        self.advance();
24529                    }
24530                    Token::RBracket => break,
24531                    other => {
24532                        return Err(self.err(alloc::format!(
24533                            "expected ',' or ']' in array literal, got {other:?}"
24534                        )));
24535                    }
24536                }
24537            }
24538        }
24539        self.advance(); // consume `]`
24540        Ok(Expr::Array(items))
24541    }
24542
24543    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24544        let mut elems = Vec::new();
24545        if matches!(self.peek(), Token::RBracket) {
24546            self.advance();
24547            return Ok(Expr::Literal(Literal::Vector(elems)));
24548        }
24549        loop {
24550            let e = self.parse_expr(0)?;
24551            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24552                message: format!("vector element must be a numeric literal, got {e:?}"),
24553                token_pos: self.pos,
24554            })?;
24555            elems.push(x);
24556            match self.peek() {
24557                Token::Comma => {
24558                    self.advance();
24559                }
24560                Token::RBracket => {
24561                    self.advance();
24562                    break;
24563                }
24564                other => {
24565                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24566                }
24567            }
24568        }
24569        Ok(Expr::Literal(Literal::Vector(elems)))
24570    }
24571
24572    /// Atom that started with an identifier: could be `t.col`, `col`, or
24573    /// `func(arg, ...)`. Detect each shape by looking at the next token.
24574    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24575    /// [, ...])`. Caller has already consumed `OVER`. Either clause
24576    /// is optional; an empty `()` is also legal (PG semantics).
24577    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24578    /// modifier between `name(args)` and `OVER (...)`. Default is
24579    /// `Respect`. Unrecognised idents leave the stream unchanged.
24580    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24581        let Token::Ident(s) = self.peek().clone() else {
24582            return NullTreatment::Respect;
24583        };
24584        let is_ignore = s.eq_ignore_ascii_case("ignore");
24585        let is_respect = s.eq_ignore_ascii_case("respect");
24586        if !is_ignore && !is_respect {
24587            return NullTreatment::Respect;
24588        }
24589        // Lookahead for NULLS — only consume both tokens together.
24590        // pos+1 must hold a "nulls" ident.
24591        if self.pos + 1 < self.tokens.len()
24592            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24593            && s2.eq_ignore_ascii_case("nulls")
24594        {
24595            self.advance();
24596            self.advance();
24597            return if is_ignore {
24598                NullTreatment::Ignore
24599            } else {
24600                NullTreatment::Respect
24601            };
24602        }
24603        NullTreatment::Respect
24604    }
24605
24606    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24607    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24608    /// (same shape as the `OVER` tail). Consumes the whole clause and
24609    /// returns the predicate; returns `None` when no `FILTER` follows.
24610    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24611        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24612            return Ok(None);
24613        };
24614        if !s.eq_ignore_ascii_case("filter") {
24615            return Ok(None);
24616        }
24617        self.advance(); // FILTER
24618        if !matches!(self.peek(), Token::LParen) {
24619            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24620        }
24621        self.advance(); // (
24622        if !matches!(self.peek(), Token::Where) {
24623            return Err(self.err(format!(
24624                "expected WHERE inside FILTER (...), got {:?}",
24625                self.peek()
24626            )));
24627        }
24628        self.advance(); // WHERE
24629        let cond = self.parse_expr(0)?;
24630        if !matches!(self.peek(), Token::RParen) {
24631            return Err(self.err(format!(
24632                "expected ')' to close FILTER (WHERE ...), got {:?}",
24633                self.peek()
24634            )));
24635        }
24636        self.advance(); // )
24637        Ok(Some(Box::new(cond)))
24638    }
24639
24640    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24641    /// the separator as the aggregate's second argument, which is the
24642    /// shape `string_agg` already takes. Returns whether one was there.
24643    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24644        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24645            return Ok(false);
24646        }
24647        self.advance();
24648        let Token::String(sep) = self.peek().clone() else {
24649            return Err(self.err(alloc::format!(
24650                "expected a string literal after SEPARATOR, got {:?}",
24651                self.peek()
24652            )));
24653        };
24654        self.advance();
24655        args.push(Expr::Literal(Literal::String(sep)));
24656        Ok(true)
24657    }
24658
24659    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24660    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24661    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24662    /// keys, or an empty vec when no `WITHIN GROUP` follows.
24663    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24664        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24665            return Ok(Vec::new());
24666        };
24667        if !s.eq_ignore_ascii_case("within") {
24668            return Ok(Vec::new());
24669        }
24670        self.advance(); // WITHIN
24671        if !matches!(self.peek(), Token::Group) {
24672            return Err(self.err(format!(
24673                "expected GROUP after WITHIN, got {:?}",
24674                self.peek()
24675            )));
24676        }
24677        self.advance(); // GROUP
24678        if !matches!(self.peek(), Token::LParen) {
24679            return Err(self.err(format!(
24680                "expected '(' after WITHIN GROUP, got {:?}",
24681                self.peek()
24682            )));
24683        }
24684        self.advance(); // (
24685        if !matches!(self.peek(), Token::Order) {
24686            return Err(self.err(format!(
24687                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24688                self.peek()
24689            )));
24690        }
24691        self.advance(); // ORDER
24692        if !self.peek_is_by() {
24693            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24694        }
24695        self.advance(); // BY
24696        let mut keys: Vec<OrderBy> = Vec::new();
24697        loop {
24698            // v7.39 (round 691) — save/restore, the discipline this parser
24699            // already uses around `pending_sample_preds`, so a subquery inside
24700            // a key neither inherits nor leaks the channel.
24701            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24702            let saved_coll = self.order_key_collation.take();
24703            let parsed = self.parse_expr(0);
24704            self.in_order_by_key = saved_flag;
24705            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24706            let expr = parsed?;
24707            let desc = if matches!(self.peek(), Token::Desc) {
24708                self.advance();
24709                true
24710            } else if matches!(self.peek(), Token::Asc) {
24711                self.advance();
24712                false
24713            } else {
24714                false
24715            };
24716            let nulls_first = self.parse_optional_nulls_placement()?;
24717            keys.push(OrderBy {
24718                expr,
24719                desc,
24720                nulls_first,
24721                collation,
24722            });
24723            if matches!(self.peek(), Token::Comma) {
24724                self.advance();
24725            } else {
24726                break;
24727            }
24728        }
24729        if !matches!(self.peek(), Token::RParen) {
24730            return Err(self.err(format!(
24731                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24732                self.peek()
24733            )));
24734        }
24735        self.advance(); // )
24736        Ok(keys)
24737    }
24738
24739    /// No frame clause is supported.
24740    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24741    fn parse_over_clause(
24742        &mut self,
24743    ) -> Result<
24744        (
24745            Vec<Expr>,
24746            Vec<(Expr, bool, Option<bool>)>,
24747            Option<WindowFrame>,
24748        ),
24749        ParseError,
24750    > {
24751        // `OVER w` — a named-window reference. The WINDOW clause
24752        // parses after the select list, so the name rides out as a
24753        // marker in partition_by; parse_bare_select substitutes the
24754        // definition once the clause is known.
24755        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24756            let name = w.clone();
24757            self.advance();
24758            return Ok((
24759                alloc::vec![Expr::Column(crate::ast::ColumnName {
24760                    qualifier: Some("__named_window__".to_string()),
24761                    name,
24762                })],
24763                Vec::new(),
24764                None,
24765            ));
24766        }
24767        if !matches!(self.peek(), Token::LParen) {
24768            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24769        }
24770        self.advance();
24771        let mut partition_by = Vec::new();
24772        let mut order_by = Vec::new();
24773        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24774        // window, refined in place. PG's rules (probed against 18.4) differ
24775        // from the bare `OVER w1` form, so the reference rides out under its
24776        // own marker and `substitute_named_windows` applies them. The base
24777        // name is any leading identifier that isn't a window-spec keyword.
24778        let base_window = match self.peek() {
24779            Token::Ident(s) | Token::QuotedIdent(s)
24780                if !s.eq_ignore_ascii_case("partition")
24781                    && !s.eq_ignore_ascii_case("rows")
24782                    && !s.eq_ignore_ascii_case("range")
24783                    && !s.eq_ignore_ascii_case("groups") =>
24784            {
24785                let n = s.clone();
24786                self.advance();
24787                Some(n)
24788            }
24789            _ => None,
24790        };
24791        // PARTITION BY ?
24792        // v7.37.6-B promoted PARTITION to a reserved keyword
24793        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
24794        // `Token::Ident("partition")`. Accept both so older sources
24795        // and the new lexer surface land on the same path.
24796        let is_partition_kw = match self.peek() {
24797            Token::Partition => true,
24798            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
24799            _ => false,
24800        };
24801        if is_partition_kw {
24802            self.advance();
24803            if !self.peek_is_by() {
24804                return Err(self.err(format!(
24805                    "expected BY after PARTITION, got {:?}",
24806                    self.peek()
24807                )));
24808            }
24809            self.advance();
24810            loop {
24811                partition_by.push(self.parse_expr(0)?);
24812                if matches!(self.peek(), Token::Comma) {
24813                    self.advance();
24814                    continue;
24815                }
24816                break;
24817            }
24818        }
24819        // ORDER BY ?
24820        if matches!(self.peek(), Token::Order) {
24821            self.advance();
24822            if !self.peek_is_by() {
24823                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24824            }
24825            self.advance();
24826            loop {
24827                let e = self.parse_expr(0)?;
24828                let desc = if matches!(self.peek(), Token::Desc) {
24829                    self.advance();
24830                    true
24831                } else if matches!(self.peek(), Token::Asc) {
24832                    self.advance();
24833                    false
24834                } else {
24835                    false
24836                };
24837                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
24838                let nulls_first = self.parse_optional_nulls_placement()?;
24839                order_by.push((e, desc, nulls_first));
24840                if matches!(self.peek(), Token::Comma) {
24841                    self.advance();
24842                    continue;
24843                }
24844                break;
24845            }
24846        }
24847        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
24848        // Both keywords come through the lexer as identifiers; match
24849        // case-insensitively.
24850        let mut frame: Option<WindowFrame> = None;
24851        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
24852            let kind = if s.eq_ignore_ascii_case("rows") {
24853                Some(FrameKind::Rows)
24854            } else if s.eq_ignore_ascii_case("range") {
24855                Some(FrameKind::Range)
24856            } else if s.eq_ignore_ascii_case("groups") {
24857                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
24858                Some(FrameKind::Groups)
24859            } else {
24860                None
24861            };
24862            if let Some(kind) = kind {
24863                self.advance();
24864                frame = Some(self.parse_frame_tail(kind)?);
24865            }
24866        }
24867        if !matches!(self.peek(), Token::RParen) {
24868            return Err(self.err(format!(
24869                "expected ')' to close OVER clause, got {:?}",
24870                self.peek()
24871            )));
24872        }
24873        self.advance();
24874        if let Some(base) = base_window {
24875            // A copy may refine but never override the base's partitioning
24876            // (PG rejects it outright, before looking the name up).
24877            if !partition_by.is_empty() {
24878                return Err(self.err(alloc::format!(
24879                    "cannot override PARTITION BY clause of window \"{base}\""
24880                )));
24881            }
24882            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
24883                qualifier: Some("__named_window_ref__".to_string()),
24884                name: base,
24885            })];
24886        }
24887        Ok((partition_by, order_by, frame))
24888    }
24889
24890    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
24891    /// or `RANGE` keyword was just consumed. Accepts both
24892    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
24893    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
24894    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
24895    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
24896        let (start, end) = if matches!(self.peek(), Token::Between) {
24897            self.advance();
24898            let start = self.parse_frame_bound()?;
24899            if !matches!(self.peek(), Token::And) {
24900                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
24901            }
24902            self.advance();
24903            let end = self.parse_frame_bound()?;
24904            (start, Some(end))
24905        } else {
24906            (self.parse_frame_bound()?, None)
24907        };
24908        let exclude = self.parse_frame_exclusion()?;
24909        Ok(WindowFrame {
24910            kind,
24911            start,
24912            end,
24913            exclude,
24914        })
24915    }
24916
24917    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
24918    /// after a frame spec. NO OTHERS is the default no-op.
24919    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
24920        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
24921            return Ok(FrameExclusion::NoOthers);
24922        }
24923        self.advance(); // EXCLUDE
24924        match self.peek() {
24925            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
24926                self.advance();
24927                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
24928                    return Err(self.err(format!(
24929                        "expected ROW after EXCLUDE CURRENT, got {:?}",
24930                        self.peek()
24931                    )));
24932                }
24933                self.advance();
24934                Ok(FrameExclusion::CurrentRow)
24935            }
24936            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
24937            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
24938            // Without this arm it fell to the catch-all, whose message
24939            // self-contradictingly listed GROUP as expected.
24940            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
24941                self.advance();
24942                Ok(FrameExclusion::Group)
24943            }
24944            Token::Group => {
24945                self.advance();
24946                Ok(FrameExclusion::Group)
24947            }
24948            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
24949                self.advance();
24950                Ok(FrameExclusion::Ties)
24951            }
24952            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
24953                self.advance();
24954                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
24955                    return Err(self.err(format!(
24956                        "expected OTHERS after EXCLUDE NO, got {:?}",
24957                        self.peek()
24958                    )));
24959                }
24960                self.advance();
24961                Ok(FrameExclusion::NoOthers)
24962            }
24963            other => Err(self.err(format!(
24964                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
24965            ))),
24966        }
24967    }
24968
24969    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
24970    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
24971    /// `UNBOUNDED FOLLOWING`.
24972    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
24973        // Interval-typed offset for a value-based RANGE frame over a
24974        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
24975        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
24976        // PRECEDING`.
24977        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
24978            let dir = self.expect_ident_like()?;
24979            return if dir.eq_ignore_ascii_case("preceding") {
24980                Ok(FrameBound::IntervalPreceding {
24981                    months,
24982                    days,
24983                    micros,
24984                })
24985            } else if dir.eq_ignore_ascii_case("following") {
24986                Ok(FrameBound::IntervalFollowing {
24987                    months,
24988                    days,
24989                    micros,
24990                })
24991            } else {
24992                Err(self.err(format!(
24993                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
24994                )))
24995            };
24996        }
24997        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
24998        if let Token::Integer(n) = *self.peek() {
24999            self.advance();
25000            let n: u64 = u64::try_from(n).map_err(|_| {
25001                self.err(format!(
25002                    "invalid frame offset {n} — expected non-negative integer"
25003                ))
25004            })?;
25005            let dir = self.expect_ident_like()?;
25006            return if dir.eq_ignore_ascii_case("preceding") {
25007                Ok(FrameBound::OffsetPreceding(n))
25008            } else if dir.eq_ignore_ascii_case("following") {
25009                Ok(FrameBound::OffsetFollowing(n))
25010            } else {
25011                Err(self.err(format!(
25012                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
25013                )))
25014            };
25015        }
25016        let first = self.expect_ident_like()?;
25017        if first.eq_ignore_ascii_case("unbounded") {
25018            let dir = self.expect_ident_like()?;
25019            return if dir.eq_ignore_ascii_case("preceding") {
25020                Ok(FrameBound::UnboundedPreceding)
25021            } else if dir.eq_ignore_ascii_case("following") {
25022                Ok(FrameBound::UnboundedFollowing)
25023            } else {
25024                Err(self.err(format!(
25025                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
25026                )))
25027            };
25028        }
25029        if first.eq_ignore_ascii_case("current") {
25030            let row = self.expect_ident_like()?;
25031            if !row.eq_ignore_ascii_case("row") {
25032                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
25033            }
25034            return Ok(FrameBound::CurrentRow);
25035        }
25036        Err(self.err(format!(
25037            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
25038        )))
25039    }
25040
25041    /// Detect and consume a leading interval offset in a frame bound —
25042    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
25043    /// `(months, days, micros)`. Leaves the cursor on the trailing
25044    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
25045    /// when the next tokens are not an interval offset.
25046    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
25047        // Shape A — `INTERVAL '1 day'`.
25048        if matches!(self.peek(), Token::Interval) {
25049            self.advance(); // INTERVAL
25050            let atom = self.parse_interval_atom()?;
25051            if let Expr::Literal(Literal::Interval {
25052                months,
25053                days,
25054                micros,
25055                ..
25056            }) = atom
25057            {
25058                return Ok(Some((months, days, micros)));
25059            }
25060            return Err(self.err("expected an interval literal in frame offset".to_string()));
25061        }
25062        // Shape B — `'1 day'::interval`. Look ahead for the exact
25063        // string / `::` / interval-target triple before committing.
25064        if let Token::String(text) = self.peek() {
25065            let target_is_interval = match self.tokens.get(self.pos + 2) {
25066                Some(Token::Interval) => true,
25067                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25068                _ => false,
25069            };
25070            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25071                && target_is_interval;
25072            if is_cast {
25073                let text = text.clone();
25074                self.advance(); // string
25075                self.advance(); // ::
25076                self.advance(); // interval
25077                let parts = parse_interval_text(&text).ok_or_else(|| {
25078                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25079                })?;
25080                return Ok(Some(parts));
25081            }
25082        }
25083        Ok(None)
25084    }
25085
25086    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25087        if matches!(self.peek(), Token::Dot) {
25088            self.advance();
25089            let name = self.expect_ident_like()?;
25090            // v7.14.0 — schema-qualified function call
25091            // `<schema>.<fn>(args)`. PG dumps emit
25092            // `pg_catalog.set_config(...)` in the preamble. SPG
25093            // is single-namespace: drop the schema prefix and
25094            // route the dispatch on the bare function name.
25095            if matches!(self.peek(), Token::LParen) {
25096                return self.finish_ident_atom(name);
25097            }
25098            return Ok(Expr::Column(ColumnName {
25099                qualifier: Some(first),
25100                name,
25101            }));
25102        }
25103        if matches!(self.peek(), Token::LParen) {
25104            self.advance();
25105            // `COUNT(*)` — special-cased here because `*` isn't a normal
25106            // expression token. Lower-case match on `first` since the lexer
25107            // folds identifiers.
25108            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25109                self.advance();
25110                if !matches!(self.peek(), Token::RParen) {
25111                    return Err(self.err(format!(
25112                        "expected ')' after COUNT(*), got {:?}",
25113                        self.peek()
25114                    )));
25115                }
25116                self.advance();
25117                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25118                let filter = self.parse_filter_clause()?;
25119                // v4.12: COUNT(*) OVER (...) — same window tail.
25120                let null_treatment = self.parse_null_treatment_modifier();
25121                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25122                    && s.eq_ignore_ascii_case("over")
25123                {
25124                    self.advance();
25125                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
25126                    return Ok(Expr::WindowFunction {
25127                        name: "count_star".into(),
25128                        args: Vec::new(),
25129                        partition_by,
25130                        order_by,
25131                        frame,
25132                        null_treatment,
25133                        filter,
25134                    });
25135                }
25136                if let Some(filter) = filter {
25137                    return Ok(Expr::AggregateOrdered {
25138                        call: Box::new(Expr::FunctionCall {
25139                            name: "count_star".into(),
25140                            args: Vec::new(),
25141                        }),
25142                        order_by: Vec::new(),
25143                        distinct: false,
25144                        filter: Some(filter),
25145                    });
25146                }
25147                return Ok(Expr::FunctionCall {
25148                    name: "count_star".into(),
25149                    args: Vec::new(),
25150                });
25151            }
25152            // Function call. PG-style: zero-or-more comma-separated args.
25153            let mut args = Vec::new();
25154            // v7.38 (read01, T14) — named-argument notation `argname => value`.
25155            // Names are collected in lock-step with `args` and resolved to
25156            // positional order after the loop (the AST stays positional).
25157            let mut arg_names: Vec<Option<String>> = Vec::new();
25158            let mut agg_order_by: Vec<OrderBy> = Vec::new();
25159            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25160            // seen, so the value arguments before it can be folded.
25161            let mut saw_separator = false;
25162            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25163            // v7.32 (round-29) — accept the dual `ALL` quantifier too
25164            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25165            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25166                self.advance();
25167                true
25168            } else if matches!(self.peek(), Token::All) {
25169                self.advance();
25170                false
25171            } else {
25172                false
25173            };
25174            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25175            // TIMESTAMPDIFF take a bare unit keyword as the first
25176            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25177            // bare type keyword (DATE / TIME / DATETIME); lower them
25178            // onto string literals so the evaluator sees plain text.
25179            if ((first.eq_ignore_ascii_case("timestampadd")
25180                || first.eq_ignore_ascii_case("timestampdiff"))
25181                && matches!(self.peek(), Token::Ident(u) if matches!(
25182                    u.to_ascii_lowercase().as_str(),
25183                    "microsecond" | "second" | "minute" | "hour" | "day"
25184                        | "week" | "month" | "quarter" | "year"
25185                )))
25186                || (first.eq_ignore_ascii_case("get_format")
25187                    && matches!(self.peek(), Token::Ident(u) if matches!(
25188                        u.to_ascii_lowercase().as_str(),
25189                        "date" | "time" | "datetime" | "timestamp"
25190                    )))
25191            {
25192                if let Token::Ident(u) = self.peek() {
25193                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25194                }
25195                self.advance();
25196                if matches!(self.peek(), Token::Comma) {
25197                    self.advance();
25198                }
25199            }
25200            // `ROW(a, b, …)` keyword constructor. Followed by a
25201            // comparison operator or [NOT] IN it joins the paren
25202            // row-constructor machinery (fieldwise parse-time
25203            // expansion); bare, it stays a `row` call the evaluator
25204            // renders as PG record text.
25205            if first.eq_ignore_ascii_case("row") {
25206                let mut row_items = Vec::new();
25207                if !matches!(self.peek(), Token::RParen) {
25208                    loop {
25209                        row_items.push(self.parse_expr(0)?);
25210                        match self.peek() {
25211                            Token::Comma => {
25212                                self.advance();
25213                            }
25214                            Token::RParen => break,
25215                            other => {
25216                                return Err(self.err(format!(
25217                                    "expected ',' or ')' in ROW(...), got {other:?}"
25218                                )));
25219                            }
25220                        }
25221                    }
25222                }
25223                self.advance(); // ')'
25224                let comparison_follows = matches!(
25225                    self.peek(),
25226                    Token::Eq
25227                        | Token::NotEq
25228                        | Token::Lt
25229                        | Token::LtEq
25230                        | Token::Gt
25231                        | Token::GtEq
25232                        | Token::In
25233                ) || (matches!(self.peek(), Token::Not)
25234                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25235                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25236                if comparison_follows && !row_items.is_empty() {
25237                    return self.parse_row_comparison_tail(row_items);
25238                }
25239                return Ok(Expr::FunctionCall {
25240                    name: String::from("row"),
25241                    args: row_items,
25242                });
25243            }
25244            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25245            // the parse-mode keyword introduces the source text. SPG
25246            // carries XML as text, so both modes lower to __xmlparse(expr)
25247            // which validates well-formedness and returns Value::Xml.
25248            if first.eq_ignore_ascii_case("xmlparse")
25249                && matches!(self.peek(), Token::Ident(kw)
25250                    if kw.eq_ignore_ascii_case("document")
25251                        || kw.eq_ignore_ascii_case("content"))
25252            {
25253                let mode = match self.advance() {
25254                    Token::Ident(kw) => kw.to_ascii_lowercase(),
25255                    _ => unreachable!("peeked an ident"),
25256                };
25257                let src = self.parse_expr(0)?;
25258                if !matches!(self.peek(), Token::RParen) {
25259                    return Err(self.err(format!(
25260                        "expected ')' to close XMLPARSE, got {:?}",
25261                        self.peek()
25262                    )));
25263                }
25264                self.advance();
25265                return Ok(Expr::FunctionCall {
25266                    name: String::from("__xmlparse"),
25267                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25268                });
25269            }
25270            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25271            // keyword introduces the element name (a bare or quoted
25272            // identifier), then optional content expressions. Lower to a
25273            // plain `xmlelement(name_text, content …)` call.
25274            if first.eq_ignore_ascii_case("xmlelement")
25275                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25276            {
25277                self.advance(); // consume NAME
25278                let elem_name = match self.peek().clone() {
25279                    Token::Ident(n) | Token::QuotedIdent(n) => {
25280                        self.advance();
25281                        n
25282                    }
25283                    other => {
25284                        return Err(self.err(format!(
25285                            "expected element name after XMLELEMENT NAME, got {other:?}"
25286                        )));
25287                    }
25288                };
25289                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25290                while matches!(self.peek(), Token::Comma) {
25291                    self.advance();
25292                    args.push(self.parse_expr(0)?);
25293                }
25294                if !matches!(self.peek(), Token::RParen) {
25295                    return Err(self.err(format!(
25296                        "expected ')' to close XMLELEMENT, got {:?}",
25297                        self.peek()
25298                    )));
25299                }
25300                self.advance();
25301                return Ok(Expr::FunctionCall {
25302                    name: String::from("xmlelement"),
25303                    args,
25304                });
25305            }
25306            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25307            // becomes a `<name>value</name>` element; a bare column infers its
25308            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25309            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25310                let mut args: Vec<Expr> = Vec::new();
25311                loop {
25312                    let val = self.parse_expr(0)?;
25313                    let name = if matches!(self.peek(), Token::As) {
25314                        self.advance();
25315                        match self.peek().clone() {
25316                            Token::Ident(n) | Token::QuotedIdent(n) => {
25317                                self.advance();
25318                                n
25319                            }
25320                            other => {
25321                                return Err(self.err(format!(
25322                                    "expected name after AS in XMLFOREST, got {other:?}"
25323                                )));
25324                            }
25325                        }
25326                    } else if let Expr::Column(c) = &val {
25327                        c.name.clone()
25328                    } else {
25329                        return Err(
25330                            self.err("XMLFOREST element without a column name needs AS".into())
25331                        );
25332                    };
25333                    args.push(Expr::Literal(Literal::String(name)));
25334                    args.push(val);
25335                    if matches!(self.peek(), Token::Comma) {
25336                        self.advance();
25337                    } else {
25338                        break;
25339                    }
25340                }
25341                if !matches!(self.peek(), Token::RParen) {
25342                    return Err(self.err(format!(
25343                        "expected ')' to close XMLFOREST, got {:?}",
25344                        self.peek()
25345                    )));
25346                }
25347                self.advance();
25348                return Ok(Expr::FunctionCall {
25349                    name: String::from("xmlforest"),
25350                    args,
25351                });
25352            }
25353            // SQL-standard `POSITION(sub IN str)` — lowers onto
25354            // strpos(str, sub). IN is the argument separator here,
25355            // so the needle parses with the IN-tail suppressed.
25356            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25357                let saved = self.suppress_in_tail;
25358                self.suppress_in_tail = true;
25359                let needle = self.parse_expr(0);
25360                self.suppress_in_tail = saved;
25361                let needle = needle?;
25362                if matches!(self.peek(), Token::In) {
25363                    self.advance();
25364                    let haystack = self.parse_expr(0)?;
25365                    if !matches!(self.peek(), Token::RParen) {
25366                        return Err(self.err(format!(
25367                            "expected ')' to close POSITION, got {:?}",
25368                            self.peek()
25369                        )));
25370                    }
25371                    self.advance();
25372                    return Ok(Expr::FunctionCall {
25373                        name: String::from("strpos"),
25374                        args: alloc::vec![haystack, needle],
25375                    });
25376                }
25377                // position(sub, str) comma form (incl. bytea) —
25378                // hand the parsed first arg to the generic list.
25379                args.push(needle);
25380                if matches!(self.peek(), Token::Comma) {
25381                    self.advance();
25382                }
25383            }
25384            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25385            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25386            // plain comma forms TRIM(str) / TRIM(str, chars) keep
25387            // riding the generic argument list below.
25388            if first.eq_ignore_ascii_case("trim") {
25389                let mode = match self.peek() {
25390                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25391                        self.advance();
25392                        Some("btrim")
25393                    }
25394                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25395                        self.advance();
25396                        Some("ltrim")
25397                    }
25398                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25399                        self.advance();
25400                        Some("rtrim")
25401                    }
25402                    _ => None,
25403                };
25404                if mode.is_some() || matches!(self.peek(), Token::From) {
25405                    // TRIM([mode] FROM str) — no strip-chars.
25406                    let chars = if matches!(self.peek(), Token::From) {
25407                        None
25408                    } else {
25409                        Some(self.parse_expr(0)?)
25410                    };
25411                    if !matches!(self.peek(), Token::From) {
25412                        return Err(self.err(format!(
25413                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25414                            self.peek()
25415                        )));
25416                    }
25417                    self.advance();
25418                    let target = self.parse_expr(0)?;
25419                    if !matches!(self.peek(), Token::RParen) {
25420                        return Err(
25421                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25422                        );
25423                    }
25424                    self.advance();
25425                    let mut trim_args = alloc::vec![target];
25426                    if let Some(c) = chars {
25427                        trim_args.push(c);
25428                    }
25429                    return Ok(Expr::FunctionCall {
25430                        name: String::from(mode.unwrap_or("btrim")),
25431                        args: trim_args,
25432                    });
25433                }
25434            }
25435            if !matches!(self.peek(), Token::RParen) {
25436                loop {
25437                    // v7.38 (read01, T14) — `argname => value` names this arg.
25438                    // v7.39 (read01 round 77) — `argname := value` is the same
25439                    // thing, and it is the spelling PG's own docs lead with. It
25440                    // was simply never lexed here, so every `f(x := 1)` died in
25441                    // the parser regardless of what `f` was.
25442                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25443                        (
25444                            Token::Ident(n) | Token::QuotedIdent(n),
25445                            Some(Token::FatArrow | Token::ColonEq),
25446                        ) => {
25447                            let name = n.clone();
25448                            self.advance(); // name
25449                            self.advance(); // => / :=
25450                            Some(name)
25451                        }
25452                        _ => None,
25453                    };
25454                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25455                    // array's elements into a variadic call's trailing args
25456                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25457                    // reserved, so it arrives as a bare ident before the arg.
25458                    let is_variadic = this_name.is_none()
25459                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25460                    if is_variadic {
25461                        self.advance();
25462                    }
25463                    let arg = self.parse_expr(0)?;
25464                    args.push(match &this_name {
25465                        // The callee's parameter names decide the slot, and a
25466                        // user function's live in the catalog. Carry the name
25467                        // to eval rather than guessing here.
25468                        Some(n) => Expr::NamedArg {
25469                            name: n.clone(),
25470                            expr: Box::new(arg),
25471                        },
25472                        None if is_variadic => Expr::Variadic(Box::new(arg)),
25473                        None => arg,
25474                    });
25475                    arg_names.push(this_name);
25476                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
25477                    // The `::` cast already worked; this lowers the
25478                    // function form onto the same Expr::Cast node.
25479                    if first.eq_ignore_ascii_case("cast")
25480                        && args.len() == 1
25481                        && matches!(self.peek(), Token::As)
25482                    {
25483                        self.advance();
25484                        let target = self.parse_cast_target()?;
25485                        if !matches!(self.peek(), Token::RParen) {
25486                            return Err(self.err(format!(
25487                                "expected ')' to close CAST, got {:?}",
25488                                self.peek()
25489                            )));
25490                        }
25491                        self.advance();
25492                        return Ok(Expr::Cast {
25493                            expr: Box::new(args.pop().expect("one arg")),
25494                            target,
25495                        });
25496                    }
25497                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25498                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25499                    // keywords; SPG's lexer makes them plain idents (so they'd be
25500                    // read as column refs). Lower the keyword to the string form
25501                    // the evaluator already accepts.
25502                    if first.eq_ignore_ascii_case("normalize")
25503                        && args.len() == 1
25504                        && matches!(self.peek(), Token::Comma)
25505                    {
25506                        let form = match self.tokens.get(self.pos + 1) {
25507                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25508                                let up = f.to_ascii_uppercase();
25509                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25510                            }
25511                            _ => None,
25512                        };
25513                        if let Some(up) = form {
25514                            self.advance(); // comma
25515                            self.advance(); // form keyword
25516                            args.push(Expr::Literal(Literal::String(up)));
25517                        }
25518                    }
25519                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25520                    // form. Desugars to the comma-list shape evaluator already
25521                    // handles. Triggered after the first arg when the function
25522                    // name is substring / substr and the next token is FROM
25523                    // (a reserved keyword in PG; SPG also reserves it).
25524                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25525                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25526                    // internal __substring_similar(str, pat, esc) call.
25527                    if (first.eq_ignore_ascii_case("substring")
25528                        || first.eq_ignore_ascii_case("substr"))
25529                        && args.len() == 1
25530                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25531                    {
25532                        self.advance(); // SIMILAR
25533                        let pattern = self.parse_expr(0)?;
25534                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25535                        {
25536                            return Err(self.err(format!(
25537                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25538                                self.peek()
25539                            )));
25540                        }
25541                        self.advance(); // ESCAPE
25542                        let esc = self.parse_expr(0)?;
25543                        if !matches!(self.peek(), Token::RParen) {
25544                            return Err(self.err(format!(
25545                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
25546                                self.peek()
25547                            )));
25548                        }
25549                        self.advance();
25550                        args.push(pattern);
25551                        args.push(esc);
25552                        return Ok(Expr::FunctionCall {
25553                            name: "__substring_similar".to_string(),
25554                            args,
25555                        });
25556                    }
25557                    if (first.eq_ignore_ascii_case("substring")
25558                        || first.eq_ignore_ascii_case("substr"))
25559                        && args.len() == 1
25560                        && matches!(self.peek(), Token::From | Token::For)
25561                    {
25562                        // `substring(str FROM pos [FOR len])`, or the FOR-only
25563                        // `substring(str FOR len)` which PG treats as FROM 1.
25564                        if matches!(self.peek(), Token::From) {
25565                            self.advance();
25566                            let start = self.parse_expr(0)?;
25567                            args.push(start);
25568                        } else {
25569                            args.push(Expr::Literal(Literal::Integer(1)));
25570                        }
25571                        if matches!(self.peek(), Token::For) {
25572                            self.advance();
25573                            let length = self.parse_expr(0)?;
25574                            args.push(length);
25575                        }
25576                        if !matches!(self.peek(), Token::RParen) {
25577                            return Err(self.err(format!(
25578                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25579                                self.peek()
25580                            )));
25581                        }
25582                        self.advance();
25583                        return Ok(Expr::FunctionCall {
25584                            name: first.to_ascii_lowercase(),
25585                            args,
25586                        });
25587                    }
25588                    // PG `overlay(str PLACING repl FROM n [FOR len])`
25589                    // syntactic form. Desugars to the `overlay(str,
25590                    // repl, n[, len])` comma-list shape the evaluator
25591                    // already implements. `PLACING` is not a reserved
25592                    // token in SPG, so it arrives as a bare Ident.
25593                    if first.eq_ignore_ascii_case("overlay")
25594                        && args.len() == 1
25595                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25596                    {
25597                        self.advance(); // consume PLACING
25598                        args.push(self.parse_expr(0)?); // replacement
25599                        if !matches!(self.peek(), Token::From) {
25600                            return Err(self.err(format!(
25601                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25602                                self.peek()
25603                            )));
25604                        }
25605                        self.advance();
25606                        args.push(self.parse_expr(0)?); // start position
25607                        if matches!(self.peek(), Token::For) {
25608                            self.advance();
25609                            args.push(self.parse_expr(0)?); // length
25610                        }
25611                        if !matches!(self.peek(), Token::RParen) {
25612                            return Err(self.err(format!(
25613                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25614                                self.peek()
25615                            )));
25616                        }
25617                        self.advance();
25618                        return Ok(Expr::FunctionCall {
25619                            name: String::from("overlay"),
25620                            args,
25621                        });
25622                    }
25623                    // `TRIM(chars FROM str)` — the keyword-less
25624                    // spelling lands here after the chars parse
25625                    // (the keyword forms return earlier).
25626                    if first.eq_ignore_ascii_case("trim")
25627                        && args.len() == 1
25628                        && matches!(self.peek(), Token::From)
25629                    {
25630                        self.advance();
25631                        let target = self.parse_expr(0)?;
25632                        if !matches!(self.peek(), Token::RParen) {
25633                            return Err(self.err(format!(
25634                                "expected ')' to close TRIM(chars FROM str), got {:?}",
25635                                self.peek()
25636                            )));
25637                        }
25638                        self.advance();
25639                        let chars = args.pop().expect("one arg");
25640                        return Ok(Expr::FunctionCall {
25641                            name: String::from("btrim"),
25642                            args: alloc::vec![target, chars],
25643                        });
25644                    }
25645                    // v7.24 (round-16 A) — aggregate-internal
25646                    // ordering: `array_agg(x ORDER BY y DESC NULLS
25647                    // LAST)`. Keys close the argument list.
25648                    if matches!(self.peek(), Token::Order) {
25649                        self.advance();
25650                        if !self.peek_is_by() {
25651                            return Err(self.err(format!(
25652                                "expected BY after ORDER in aggregate args, got {:?}",
25653                                self.peek()
25654                            )));
25655                        }
25656                        self.advance();
25657                        loop {
25658                            // v7.39 (round 691) — save/restore, the discipline this parser
25659                            // already uses around `pending_sample_preds`, so a subquery inside
25660                            // a key neither inherits nor leaks the channel.
25661                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25662                            let saved_coll = self.order_key_collation.take();
25663                            let parsed = self.parse_expr(0);
25664                            self.in_order_by_key = saved_flag;
25665                            let collation =
25666                                core::mem::replace(&mut self.order_key_collation, saved_coll);
25667                            let expr = parsed?;
25668                            let desc = if matches!(self.peek(), Token::Desc) {
25669                                self.advance();
25670                                true
25671                            } else if matches!(self.peek(), Token::Asc) {
25672                                self.advance();
25673                                false
25674                            } else {
25675                                false
25676                            };
25677                            let nulls_first = self.parse_optional_nulls_placement()?;
25678                            agg_order_by.push(OrderBy {
25679                                expr,
25680                                desc,
25681                                nulls_first,
25682                                collation,
25683                            });
25684                            if matches!(self.peek(), Token::Comma) {
25685                                self.advance();
25686                            } else {
25687                                break;
25688                            }
25689                        }
25690                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25691                        // follow the ORDER BY inside GROUP_CONCAT.
25692                        if self.consume_group_concat_separator(&mut args)? {
25693                            saw_separator = true;
25694                        }
25695                        if !matches!(self.peek(), Token::RParen) {
25696                            return Err(self.err(format!(
25697                                "expected ')' after aggregate ORDER BY, got {:?}",
25698                                self.peek()
25699                            )));
25700                        }
25701                        break;
25702                    }
25703                    // v7.39 (round 354, M12) — …or directly after the
25704                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25705                    // own spelling of what PG passes as string_agg's second
25706                    // argument; it was a parse error, so every MySQL query
25707                    // that names its own separator failed outright.
25708                    if self.consume_group_concat_separator(&mut args)? {
25709                        saw_separator = true;
25710                        break;
25711                    }
25712                    match self.peek() {
25713                        Token::Comma => {
25714                            self.advance();
25715                        }
25716                        Token::RParen => break,
25717                        other => {
25718                            return Err(self.err(format!(
25719                                "expected ',' or ')' in function args, got {other:?}"
25720                            )));
25721                        }
25722                    }
25723                }
25724            }
25725            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25726            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25727            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25728            // meaning a separator — that is what the explicit SEPARATOR
25729            // tail is for. Fold them into one `concat(...)` so the
25730            // aggregate keeps its single value argument.
25731            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25732                let values = args.len() - usize::from(saw_separator);
25733                if values > 1 {
25734                    let sep_arg = if saw_separator { args.pop() } else { None };
25735                    let folded = Expr::FunctionCall {
25736                        name: "concat".to_string(),
25737                        args: core::mem::take(&mut args),
25738                    };
25739                    args.push(folded);
25740                    if let Some(sep) = sep_arg {
25741                        args.push(sep);
25742                    }
25743                }
25744            }
25745            self.advance(); // consume ')'
25746            // v7.39 (read01 round 77) — named arguments are NOT reordered here
25747            // any more. The parser has no catalog, so it could only ever resolve
25748            // the handful of `make_*` builtins whose parameter names were baked
25749            // into a table right here — every user function got
25750            // "does not support named arguments", though the catalog has been
25751            // storing its parameter names all along. Reordering happens in eval,
25752            // in one place, for builtins and user functions alike.
25753            // v7.32 (round-29) — ordered-set aggregate tail
25754            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25755            // (percentile_cont / percentile_disc / mode). The sort spec
25756            // lands in the same `order_by` slot a decorated aggregate
25757            // uses; the executor dispatches on the function name. WITHIN
25758            // GROUP and an intra-argument ORDER BY are mutually
25759            // exclusive (PG rejects both).
25760            let within_group_order = self.parse_within_group_clause()?;
25761            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25762                return Err(self.err(
25763                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25764                        .into(),
25765                ));
25766            }
25767            let within_group_seen = !within_group_order.is_empty();
25768            let agg_order_by = if within_group_order.is_empty() {
25769                agg_order_by
25770            } else {
25771                within_group_order
25772            };
25773            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25774            let filter = self.parse_filter_clause()?;
25775            // v4.12: window-function tail — `name(args) OVER (...)`.
25776            // Promotes the just-parsed FunctionCall into a
25777            // WindowFunction node carrying partition + order.
25778            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25779            // / `RESPECT NULLS OVER (...)` between the closing paren
25780            // and `OVER`.
25781            let null_treatment = self.parse_null_treatment_modifier();
25782            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25783                && s.eq_ignore_ascii_case("over")
25784            {
25785                self.advance();
25786                // v7.39 (round 230) — PG implements neither modifier for a
25787                // windowed call and says so (0A000). Both used to be parsed
25788                // and then silently dropped here, so `count(DISTINCT v)
25789                // OVER (…)` quietly answered the non-distinct count.
25790                if agg_distinct {
25791                    return Err(
25792                        self.err("DISTINCT is not implemented for window functions".to_string())
25793                    );
25794                }
25795                if !agg_order_by.is_empty() {
25796                    // PG separates the two shapes that land here: a
25797                    // WITHIN GROUP call is an ordered-set aggregate and gets
25798                    // its own message naming the aggregate; a plain
25799                    // `agg(x ORDER BY y)` gets the generic one.
25800                    let msg = if within_group_seen {
25801                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
25802                    } else {
25803                        "aggregate ORDER BY is not implemented for window functions".to_string()
25804                    };
25805                    return Err(self.err(msg));
25806                }
25807                let (partition_by, order_by, frame) = self.parse_over_clause()?;
25808                return Ok(Expr::WindowFunction {
25809                    name: first,
25810                    args,
25811                    partition_by,
25812                    order_by,
25813                    frame,
25814                    null_treatment,
25815                    filter,
25816                });
25817            }
25818            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
25819                return Ok(Expr::AggregateOrdered {
25820                    call: Box::new(Expr::FunctionCall { name: first, args }),
25821                    order_by: agg_order_by,
25822                    distinct: agg_distinct,
25823                    filter,
25824                });
25825            }
25826            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
25827            // over TIMESTAMPTZ and has no timestamp overload, so a
25828            // timestamp argument is coerced on the way in and the answer
25829            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
25830            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
25831            // zone`. SPG answered `timestamp without time zone`, dropping
25832            // the offset from every rendering.
25833            //
25834            // Writing the coercion PG performs makes the existing
25835            // argument-driven typing (the one `date_trunc` uses) reach the
25836            // right answer, rather than teaching the type layer a second
25837            // rule. MySQL's DATE_ADD is a different function that returns
25838            // DATE or DATETIME, so this is PG-dialect only.
25839            //
25840            // Out-of-line because this sits on the RECURSIVE descent
25841            // frame: an inline block with locals here costs every nesting
25842            // level, and the suite's deep-nesting sentinel overflowed the
25843            // 512 KiB parser stack the moment one was added (round 430's
25844            // lesson, in the same shape).
25845            if !self.mysql_dialect {
25846                lift_date_add_arg_to_timestamptz(&first, &mut args);
25847            }
25848            return Ok(Expr::FunctionCall { name: first, args });
25849        }
25850        // v7.9.20 — SQL-standard parenless keyword expressions
25851        // (PG treats these as functions called without parens).
25852        // Resolve to a synthetic FunctionCall so the engine's
25853        // eval path reuses the existing function-call routing.
25854        // mailrs G3.
25855        let lc = first.to_ascii_lowercase();
25856        if matches!(
25857            lc.as_str(),
25858            "current_date"
25859                | "current_time"
25860                | "current_timestamp"
25861                | "localtimestamp"
25862                | "localtime"
25863                // v7.37.17 (17.6 siblings) — session-identity SQL-
25864                // standard parenless keywords. current_user /
25865                // session_user / user were already caught by the
25866                // pgwire canned-response shortcut but bare-select
25867                // in the embedded engine went through Expr::Column
25868                // and errored. Adding them here so the parser
25869                // resolves to a synthetic FunctionCall that reuses
25870                // the existing eval/functions.rs dispatch.
25871                | "current_user"
25872                | "session_user"
25873                | "current_role"
25874                | "current_catalog"
25875                | "current_schema"
25876                | "current_database"
25877                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
25878                | "system_user"
25879        ) {
25880            return Ok(Expr::FunctionCall {
25881                name: lc,
25882                args: Vec::new(),
25883            });
25884        }
25885        Ok(Expr::Column(ColumnName {
25886            qualifier: None,
25887            name: first,
25888        }))
25889    }
25890}
25891
25892/// v7.39 (round 522) — write the coercion PG's `date_add` /
25893/// `date_subtract` signature performs.
25894///
25895/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
25896/// timestamp argument is cast on the way in and the answer is
25897/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
25898/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
25899/// `timestamp without time zone`, dropping the offset from every
25900/// rendering of the result.
25901///
25902/// Writing the cast the signature implies lets the existing
25903/// argument-driven typing (the one `date_trunc` uses) reach the right
25904/// answer instead of teaching the type layer a second rule. MySQL's
25905/// DATE_ADD is a different function returning DATE or DATETIME, so the
25906/// caller applies this in PG dialect only.
25907///
25908/// A free function, and not a block at the call site, because the caller
25909/// is on the recursive-descent frame chain.
25910#[inline(never)]
25911fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
25912    if args.len() != 2
25913        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
25914    {
25915        return;
25916    }
25917    let base = args.remove(0);
25918    args.insert(
25919        0,
25920        Expr::Cast {
25921            expr: Box::new(base),
25922            target: CastTarget::Timestamptz,
25923        },
25924    );
25925}
25926
25927/// v6.8.2 — walk an expression tree and return the first column
25928/// reference's bare name. Used by `parse_create_index_stmt_after_create`
25929/// to derive `CreateIndexStatement.column` from an expression
25930/// key (so downstream planner code resolving a primary column
25931/// position keeps working with expression indexes). Returns
25932/// `None` when the expression has no column ref at all — caller
25933/// surfaces that as a parse error.
25934fn extract_first_column(expr: &Expr) -> Option<String> {
25935    match expr {
25936        Expr::Column(cn) => Some(cn.name.clone()),
25937        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
25938        Expr::Binary { lhs, rhs, .. } => {
25939            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
25940        }
25941        Expr::Unary { expr: e, .. } => extract_first_column(e),
25942        // v7.39 (read01 round 93) — a cast wraps its operand: a common
25943        // expression-index key is `lower(col::text)`, where the column
25944        // sits under the `::text` cast inside the function arg. Without
25945        // descending here the key was rejected as "references no column".
25946        Expr::Cast { expr: e, .. } => extract_first_column(e),
25947        _ => None,
25948    }
25949}
25950
25951fn maybe_not(expr: Expr, negated: bool) -> Expr {
25952    if negated {
25953        Expr::Unary {
25954            op: UnOp::Not,
25955            expr: Box::new(expr),
25956        }
25957    } else {
25958        expr
25959    }
25960}
25961
25962/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
25963/// things in the two dialects, and SPG read all three PG's way:
25964///
25965/// | token | PG (and SPG) | MySQL, measured |
25966/// |---|---|---|
25967/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
25968/// | `&&` | inet / array overlap | **AND** |
25969/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
25970///
25971/// `1 || 0` answering the string '10' on a MySQL session is a wrong
25972/// answer with no error, which is why they are routed here rather than
25973/// left to the shared table.
25974impl Parser {
25975    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
25976        if self.mysql_dialect {
25977            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
25978            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
25979            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
25980            if let Token::Ident(w) = tok
25981                && w.eq_ignore_ascii_case("div")
25982            {
25983                return Some((BinOp::IntDiv, 8));
25984            }
25985            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
25986            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
25987            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
25988            // there sits in operand position, not infix).
25989            if let Token::Ident(w) = tok
25990                && w.eq_ignore_ascii_case("mod")
25991            {
25992                return Some((BinOp::Mod, 8));
25993            }
25994            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
25995            // plain ident to the lexer. Its precedence sits between OR (1)
25996            // and AND (3) — hence rung 2, the slot freed by moving AND up.
25997            if let Token::Ident(w) = tok
25998                && w.eq_ignore_ascii_case("xor")
25999            {
26000                return Some((BinOp::LogicalXor, 2));
26001            }
26002            match tok {
26003                Token::Concat => return Some((BinOp::Or, 1)),
26004                // MySQL's `&&` is logical AND, sharing AND's rung (3).
26005                Token::InetOverlap => return Some((BinOp::And, 3)),
26006                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
26007                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
26008                _ => {}
26009            }
26010        }
26011        binop_from(tok)
26012    }
26013}
26014
26015// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
26016// (which sits strictly between OR and AND), every level from AND upward was
26017// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
26018// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
26019// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
26020// the *relative* order of every PG operator is unchanged by the shift.
26021fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
26022    let pair = match tok {
26023        Token::Or => (BinOp::Or, 1),
26024        Token::And => (BinOp::And, 3),
26025        Token::Eq => (BinOp::Eq, 5),
26026        Token::NotEq => (BinOp::NotEq, 5),
26027        Token::Lt => (BinOp::Lt, 5),
26028        Token::LtEq => (BinOp::LtEq, 5),
26029        Token::Gt => (BinOp::Gt, 5),
26030        Token::GtEq => (BinOp::GtEq, 5),
26031        // pgvector distance ops all sit on the same rung — tighter than
26032        // comparisons (5) so `col <-> v < threshold` parses correctly.
26033        Token::L2Distance => (BinOp::L2Distance, 6),
26034        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
26035        // comparison rung.
26036        Token::GeomParallel => (BinOp::GeomParallel, 5),
26037        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
26038        // comparison rung.
26039        Token::OverLeft => (BinOp::OverLeft, 5),
26040        Token::OverRight => (BinOp::OverRight, 5),
26041        Token::GeomPerp => (BinOp::GeomPerp, 5),
26042        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
26043        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
26044        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
26045        Token::InnerProduct => (BinOp::InnerProduct, 6),
26046        Token::CosineDistance => (BinOp::CosineDistance, 6),
26047        Token::Plus => (BinOp::Add, 7),
26048        Token::Minus => (BinOp::Sub, 7),
26049        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
26050        // binds every "other" operator (`||`, `|`, `&`, `#`, the
26051        // pgvector distances above) BETWEEN additive (7) and the
26052        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
26053        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
26054        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
26055        // ("matches PG conceptually" — the round-753 audit measured it
26056        // false; the old rung errored on `'a' || 1 + 1` with
26057        // `text + integer`). Same-level chains left-fold, as PG does.
26058        Token::Concat => (BinOp::Concat, 6),
26059        Token::Pipe => (BinOp::BitOr, 6),
26060        Token::Amp => (BinOp::BitAnd, 6),
26061        Token::Star => (BinOp::Mul, 8),
26062        Token::Slash => (BinOp::Div, 8),
26063        Token::Percent => (BinOp::Mod, 8),
26064        // v4.14: JSON path ops bind tighter than comparisons (5)
26065        // and additive (7) so `doc->'k' = 'v'` parses correctly.
26066        // Same rung as the multiplicative ops.
26067        Token::JsonGet => (BinOp::JsonGet, 8),
26068        Token::JsonGetText => (BinOp::JsonGetText, 8),
26069        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26070        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26071        Token::JsonContains => (BinOp::JsonContains, 8),
26072        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26073        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26074        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26075        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26076        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26077        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26078        // v7.12.2 — `@@` binds at the comparison rung (looser than
26079        // arithmetic, tighter than AND / OR). PG places `@@` at
26080        // the same precedence as `=` / `<`, so we follow.
26081        Token::TsMatch => (BinOp::TsMatch, 5),
26082        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26083        // PG places these at the comparison rung (same level as `=`),
26084        // so we follow.
26085        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26086        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26087        Token::InetContains => (BinOp::InetContains, 5),
26088        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26089        Token::InetOverlap => (BinOp::InetOverlap, 5),
26090        // v7.39 (round 508) — the geometric and pattern-order predicates
26091        // ride the comparison rung, as every other predicate does.
26092        Token::Intersects => (BinOp::Intersects, 5),
26093        Token::IsBelow => (BinOp::IsBelow, 5),
26094        Token::IsAbove => (BinOp::IsAbove, 5),
26095        Token::PatternLt => (BinOp::PatternLt, 5),
26096        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26097        Token::PatternGt => (BinOp::PatternGt, 5),
26098        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26099        // `@@@` is the old spelling of `@@` and means exactly it.
26100        Token::TsMatchOld => (BinOp::TsMatch, 5),
26101        _ => return None,
26102    };
26103    Some(pair)
26104}
26105
26106#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26107// `as f32` here is intentional: vector elements widen / narrow into f32 on
26108// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26109// past ~15 decimal digits — both are acceptable for a fixed-precision
26110// pgvector column.
26111/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26112/// implicit table alias and break trailing clauses. WITH lands
26113/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26114/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26115/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26116/// / VALUES / FOR / LATERAL — all of which would otherwise be
26117/// silently swallowed by `parse_optional_alias`.
26118fn is_alias_stopword(s: &str) -> bool {
26119    matches!(
26120        s.to_ascii_lowercase().as_str(),
26121        "with"
26122            | "on"
26123            | "where"
26124            | "having"
26125            | "group"
26126            | "order"
26127            | "limit"
26128            | "offset"
26129            | "union"
26130            | "except"
26131            | "intersect"
26132            | "returning"
26133            | "set"
26134            | "values"
26135            | "for"
26136            | "window"
26137            | "tablesample"
26138            | "lateral"
26139            | "left"
26140            | "right"
26141            | "inner"
26142            | "outer"
26143            | "full"
26144            | "cross"
26145            | "join"
26146            | "natural"
26147            | "using"
26148            | "fetch"
26149    )
26150}
26151
26152fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26153    match e {
26154        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26155        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26156        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26157        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26158        // so scale the divisor by hand instead of `f32::powi`.)
26159        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26160            let mut div = 1.0f32;
26161            for _ in 0..*scale {
26162                div *= 10.0;
26163            }
26164            Some(*unscaled as f32 / div)
26165        }
26166        Expr::Unary {
26167            op: UnOp::Neg,
26168            expr,
26169        } => extract_numeric_literal(expr).map(|x| -x),
26170        _ => None,
26171    }
26172}
26173
26174/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26175/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26176/// negative. Returns `None` if any pair fails to parse or no pair is found.
26177///
26178/// Recognised units (case-insensitive, optional trailing `s`):
26179/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26180/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26181/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26182/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26183/// (PG-canonical: DST and month-boundary semantics depend on this).
26184/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26185/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26186/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26187#[allow(clippy::cast_possible_truncation)]
26188fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26189    let mut months: i64 = 0;
26190    let mut days: i64 = 0;
26191    let mut micros: i64 = 0;
26192    let mut in_time = false;
26193    let mut num = alloc::string::String::new();
26194    for ch in rest.chars() {
26195        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26196            num.push(ch);
26197            continue;
26198        }
26199        if ch == 'T' || ch == 't' {
26200            if !num.is_empty() {
26201                return None;
26202            }
26203            in_time = true;
26204            continue;
26205        }
26206        let n: f64 = num.parse().ok()?;
26207        num.clear();
26208        match (ch, in_time) {
26209            ('Y' | 'y', false) => months += (n * 12.0) as i64,
26210            ('M', false) => months += n as i64,
26211            ('W' | 'w', false) => days += (n * 7.0) as i64,
26212            ('D' | 'd', false) => days += n as i64,
26213            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26214            ('M', true) => micros += (n * 60_000_000.0) as i64,
26215            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26216            _ => return None,
26217        }
26218    }
26219    if !num.is_empty() {
26220        return None;
26221    }
26222    Some((
26223        i32::try_from(months).ok()?,
26224        i32::try_from(days).ok()?,
26225        micros,
26226    ))
26227}
26228
26229/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26230/// leading `-` negates the whole value). Rejects date-like strings.
26231fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26232    let (neg, body) = match s.strip_prefix('-') {
26233        Some(b) => (true, b),
26234        None => (false, s),
26235    };
26236    let (y, m) = body.split_once('-')?;
26237    let years: i32 = y.parse().ok()?;
26238    let mons: i32 = m.parse().ok()?;
26239    if years < 0 || mons < 0 {
26240        return None;
26241    }
26242    let total = years.checked_mul(12)?.checked_add(mons)?;
26243    Some((if neg { -total } else { total }, 0, 0))
26244}
26245
26246/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26247/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26248fn parse_interval_clock(tok: &str) -> Option<i64> {
26249    let (neg, body) = match tok.strip_prefix('-') {
26250        Some(r) => (true, r),
26251        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26252    };
26253    let mut it = body.split(':');
26254    let h: i64 = it.next()?.parse().ok()?;
26255    let m: i64 = it.next()?.parse().ok()?;
26256    let s_tok = it.next().unwrap_or("0");
26257    if it.next().is_some() {
26258        return None;
26259    }
26260    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26261        let sec: i64 = sec.parse().ok()?;
26262        let mut f = alloc::string::String::from(frac);
26263        while f.len() < 6 {
26264            f.push('0');
26265        }
26266        f.truncate(6);
26267        let fus: i64 = f.parse().ok()?;
26268        sec.checked_mul(1_000_000)?.checked_add(fus)?
26269    } else {
26270        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26271    };
26272    let total = h
26273        .checked_mul(3_600_000_000)?
26274        .checked_add(m.checked_mul(60_000_000)?)?
26275        .checked_add(sec_us)?;
26276    Some(if neg { -total } else { total })
26277}
26278
26279/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26280/// every spelling PG accepts (measured against live PG18.4, not guessed):
26281/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26282/// Before this, the unit table matched long names only, with an ad-hoc
26283/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26284/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26285/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26286/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26287/// fractional) both read from this one table now.
26288fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26289    let u = raw.to_ascii_lowercase();
26290    Some(match u.as_str() {
26291        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26292            "microsecond"
26293        }
26294        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26295            "millisecond"
26296        }
26297        "second" | "seconds" | "sec" | "secs" | "s" => "second",
26298        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26299        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26300        "day" | "days" | "d" => "day",
26301        "week" | "weeks" | "w" => "week",
26302        "month" | "months" | "mon" | "mons" => "month",
26303        "year" | "years" | "yr" | "yrs" | "y" => "year",
26304        "decade" | "decades" | "dec" | "decs" => "decade",
26305        "century" | "centuries" | "cent" | "c" => "century",
26306        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26307        _ => return None,
26308    })
26309}
26310
26311/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26312/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26314pub(crate) enum IntervalField {
26315    Year,
26316    Month,
26317    Day,
26318    Hour,
26319    Minute,
26320    Second,
26321}
26322
26323/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26324/// spellings aren't standard for the qualifier position, so only the singular
26325/// forms are accepted.
26326/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26327/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26328/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26329/// take a `'1 2'` style literal — are not read here; they stay a parse
26330/// error rather than being silently misread.)
26331/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26332///
26333/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26334/// to do with a `@@` engine setting, and an unset one reads NULL rather
26335/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26336/// were the same node and `SELECT @x` answered "Unknown system variable".)
26337/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26338/// not see a session override — measured, after `SET autocommit=0`,
26339/// `@@global.autocommit` is still 1.
26340///
26341/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26342/// the parser's nesting budget is tuned against, and building these
26343/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26344/// wall `parse_left_right_atom` and friends were factored out for).
26345#[inline(never)]
26346fn variable_ref_atom(raw: &str) -> Expr {
26347    let user_var = !raw.starts_with("@@");
26348    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26349    Expr::FunctionCall {
26350        name: String::from(if user_var {
26351            "__spg_user_var"
26352        } else {
26353            "__spg_session_var"
26354        }),
26355        args: alloc::vec![Expr::Literal(Literal::String(bare))],
26356    }
26357}
26358
26359fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26360    let Token::Ident(s) = tok else { return None };
26361    Some(match () {
26362        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26363        () if s.eq_ignore_ascii_case("second") => "second",
26364        () if s.eq_ignore_ascii_case("minute") => "minute",
26365        () if s.eq_ignore_ascii_case("hour") => "hour",
26366        () if s.eq_ignore_ascii_case("day") => "day",
26367        () if s.eq_ignore_ascii_case("week") => "week",
26368        () if s.eq_ignore_ascii_case("month") => "month",
26369        () if s.eq_ignore_ascii_case("quarter") => "quarter",
26370        () if s.eq_ignore_ascii_case("year") => "year",
26371        () => return None,
26372    })
26373}
26374
26375/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26376/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26377/// which constructs the value at run time. Only the slot the unit names
26378/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26379/// slot the builtin has (months and fractional seconds respectively).
26380fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26381    let zero = || Expr::Literal(Literal::Integer(0));
26382    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26383        lhs: alloc::boxed::Box::new(qty.clone()),
26384        op,
26385        rhs: alloc::boxed::Box::new(by),
26386    };
26387    // (years, months, weeks, days, hours, mins, secs)
26388    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26389    match unit {
26390        "year" => args[0] = qty,
26391        "quarter" => {
26392            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26393        }
26394        "month" => args[1] = qty,
26395        "week" => args[2] = qty,
26396        "day" => args[3] = qty,
26397        "hour" => args[4] = qty,
26398        "minute" => args[5] = qty,
26399        "second" => args[6] = qty,
26400        // The builtin's seconds slot takes a fraction, so microseconds ride
26401        // it scaled down; the divisor is a NUMERIC literal so the division
26402        // stays exact rather than going through a float.
26403        "microsecond" => {
26404            args[6] = scaled(
26405                crate::ast::BinOp::Div,
26406                Expr::Literal(Literal::Numeric {
26407                    unscaled: 1_000_000,
26408                    scale: 0,
26409                }),
26410            );
26411        }
26412        _ => args[3] = qty,
26413    }
26414    Expr::FunctionCall {
26415        name: alloc::string::String::from("make_interval"),
26416        args,
26417    }
26418}
26419
26420/// `(count, unit)` → `(months, days, micros)`.
26421fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26422    let n: i64 = count.trim().parse().ok()?;
26423    Some(match unit {
26424        "microsecond" => (0, 0, n),
26425        "second" => (0, 0, n.checked_mul(1_000_000)?),
26426        "minute" => (0, 0, n.checked_mul(60_000_000)?),
26427        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26428        "day" => (0, i32::try_from(n).ok()?, 0),
26429        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26430        "month" => (i32::try_from(n).ok()?, 0, 0),
26431        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26432        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26433        _ => return None,
26434    })
26435}
26436
26437fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26438    let Token::Ident(s) = tok else { return None };
26439    Some(match () {
26440        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26441        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26442        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26443        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26444        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26445        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26446        () => return None,
26447    })
26448}
26449
26450/// v7.39 (read01 round 102) — interpret an interval literal under a field
26451/// qualifier. Returns `(months, days, micros)`.
26452///
26453/// * A single field applied to a bare number sets which unit the number means,
26454///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26455///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26456/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26457/// * Every other range, and any literal a single field can't read as a plain
26458///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26459///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26460///   like PG, and the qualifier there only bounds precision.
26461fn interpret_qualified_interval(
26462    text: &str,
26463    (f1, f2): (IntervalField, Option<IntervalField>),
26464) -> Option<(i32, i32, i64)> {
26465    if let Some(f2) = f2 {
26466        if f1 == IntervalField::Year && f2 == IntervalField::Month {
26467            if let Some(m) = parse_year_month_literal(text) {
26468                return Some((m, 0, 0));
26469            }
26470        }
26471        return parse_interval_text(text);
26472    }
26473    // Single field: reinterpret a bare number; otherwise the default parse.
26474    let trimmed = text.trim();
26475    if let Ok(val) = trimmed.parse::<f64>() {
26476        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26477        #[allow(clippy::cast_possible_truncation)]
26478        let whole = val as i64;
26479        #[allow(clippy::cast_possible_truncation)]
26480        let secs_micros = {
26481            let m = val * 1_000_000.0;
26482            if m >= 0.0 {
26483                (m + 0.5) as i64
26484            } else {
26485                (m - 0.5) as i64
26486            }
26487        };
26488        return Some(match f1 {
26489            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26490            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26491            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26492            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26493            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26494            IntervalField::Second => (0, 0, secs_micros),
26495        });
26496    }
26497    parse_interval_text(text)
26498}
26499
26500/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26501fn parse_year_month_literal(text: &str) -> Option<i32> {
26502    let t = text.trim();
26503    let (neg, body) = match t.strip_prefix('-') {
26504        Some(r) => (true, r),
26505        None => (false, t.strip_prefix('+').unwrap_or(t)),
26506    };
26507    let mut it = body.split('-');
26508    let years: i32 = it.next()?.trim().parse().ok()?;
26509    let months: i32 = match it.next() {
26510        Some(m) => m.trim().parse().ok()?,
26511        None => 0,
26512    };
26513    if it.next().is_some() {
26514        return None;
26515    }
26516    let total = years.checked_mul(12)?.checked_add(months)?;
26517    Some(if neg { -total } else { total })
26518}
26519
26520pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26521    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26522    // `@` is decorative; a trailing `ago` negates the whole interval.
26523    let mut trimmed = s.trim();
26524    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26525    let mut negate = false;
26526    if let Some(rest) = trimmed
26527        .strip_suffix("ago")
26528        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26529    {
26530        negate = true;
26531        trimmed = rest.trim();
26532    }
26533    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26534        let (mo, d, us) = v?;
26535        if negate {
26536            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26537        } else {
26538            Some((mo, d, us))
26539        }
26540    };
26541    let s = trimmed;
26542    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26543    // are single tokens, not the `<n> <unit>` pair form handled below.
26544    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26545        return finish(parse_iso8601_interval(rest));
26546    }
26547    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26548        if let Some(iv) = parse_year_month_interval(trimmed) {
26549            return finish(Some(iv));
26550        }
26551    }
26552    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26553    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26554    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26555    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26556        if let Ok(n) = trimmed.parse::<i64>() {
26557            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26558        }
26559        if let Ok(f) = trimmed.parse::<f64>() {
26560            if f.is_finite() {
26561                #[allow(clippy::cast_possible_truncation)]
26562                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26563            }
26564        }
26565    }
26566    // v7.39 (round 243) — PG accepts the number and unit run together
26567    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26568    // the `<n> <unit>` pair loop below sees them as two.
26569    let raw_parts: Vec<&str> = s.split_whitespace().collect();
26570    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26571    for p in raw_parts {
26572        let boundary = p
26573            .char_indices()
26574            .find(|(i, c)| {
26575                *i > 0
26576                    && c.is_ascii_alphabetic()
26577                    && p[..*i]
26578                        .chars()
26579                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26580                    && p[..*i].chars().any(|d| d.is_ascii_digit())
26581            })
26582            .map(|(i, _)| i);
26583        match boundary {
26584            Some(i) => {
26585                parts.push(&p[..i]);
26586                parts.push(&p[i..]);
26587            }
26588            None => parts.push(p),
26589        }
26590    }
26591    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26592    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26593    // remains is the `<n> <unit>` pair form handled below.
26594    let mut clock_us: i64 = 0;
26595    let mut had_clock = false;
26596    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26597        clock_us = parse_interval_clock(parts[pos])?;
26598        parts.remove(pos);
26599        had_clock = true;
26600    }
26601    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26602    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26603    let mut lone_days: i32 = 0;
26604    if had_clock && parts.len() == 1 {
26605        if let Ok(n) = parts[0].parse::<i64>() {
26606            lone_days = i32::try_from(n).ok()?;
26607            parts.clear();
26608        }
26609    }
26610    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26611        return None;
26612    }
26613    let mut months: i32 = 0;
26614    let mut days: i32 = lone_days;
26615    let mut micros: i64 = clock_us;
26616    let mut i = 0;
26617    while i < parts.len() {
26618        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26619        if let Ok(n) = parts[i].parse::<i64>() {
26620            match unit_stripped {
26621                "microsecond" => micros = micros.checked_add(n)?,
26622                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26623                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26624                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26625                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26626                "day" => {
26627                    let n32 = i32::try_from(n).ok()?;
26628                    days = days.checked_add(n32)?;
26629                }
26630                "week" => {
26631                    let n32 = i32::try_from(n).ok()?;
26632                    days = days.checked_add(n32.checked_mul(7)?)?;
26633                }
26634                "month" => {
26635                    let n32 = i32::try_from(n).ok()?;
26636                    months = months.checked_add(n32)?;
26637                }
26638                "year" => {
26639                    let n32 = i32::try_from(n).ok()?;
26640                    months = months.checked_add(n32.checked_mul(12)?)?;
26641                }
26642                // v7.39 (read01 timestamp.c) — the larger calendar units.
26643                "decade" => {
26644                    let n32 = i32::try_from(n).ok()?;
26645                    months = months.checked_add(n32.checked_mul(120)?)?;
26646                }
26647                "century" => {
26648                    let n32 = i32::try_from(n).ok()?;
26649                    months = months.checked_add(n32.checked_mul(1200)?)?;
26650                }
26651                "millennium" => {
26652                    let n32 = i32::try_from(n).ok()?;
26653                    months = months.checked_add(n32.checked_mul(12000)?)?;
26654                }
26655                _ => return None,
26656            }
26657        } else if let Ok(f) = parts[i].parse::<f64>() {
26658            // Fractional units cascade down to the next-finer field the way
26659            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26660            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26661            // no_std: f64 has no trunc/fract/round methods, so do them with
26662            // casts (toward-zero) + explicit round-half-away-from-zero.
26663            #[allow(clippy::cast_possible_truncation)]
26664            fn round_i64(x: f64) -> i64 {
26665                if x >= 0.0 {
26666                    (x + 0.5) as i64
26667                } else {
26668                    (x - 0.5) as i64
26669                }
26670            }
26671            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26672            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26673                const DAY_US: f64 = 86_400_000_000.0;
26674                let whole = d as i64; // truncates toward zero
26675                let frac = d - whole as f64;
26676                *days = days.checked_add(i32::try_from(whole).ok()?)?;
26677                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26678                Some(())
26679            }
26680            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26681            match unit_stripped {
26682                "microsecond" => micros = micros.checked_add(round_i64(f))?,
26683                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26684                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26685                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26686                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26687                "day" => add_days_frac(&mut days, &mut micros, f)?,
26688                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26689                "month" => {
26690                    let whole = f as i64;
26691                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26692                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26693                }
26694                "year" => {
26695                    let m = f * 12.0;
26696                    let whole = m as i64;
26697                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26698                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26699                }
26700                _ => return None,
26701            }
26702        } else {
26703            return None;
26704        }
26705        i += 2;
26706    }
26707    finish(Some((months, days, micros)))
26708}
26709
26710/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26711/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26712/// `interval` is intentionally absent (handled by its own parser arm).
26713/// Returns `None` for names that aren't sensible as a bare typed literal, so
26714/// the caller falls back to treating the ident as a column reference.
26715fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26716    Some(match ident {
26717        "date" => CastTarget::Date,
26718        "timestamp" | "datetime" => CastTarget::Timestamp,
26719        "timestamptz" => CastTarget::Timestamptz,
26720        "bool" | "boolean" => CastTarget::Bool,
26721        "int" | "integer" | "int4" => CastTarget::Int,
26722        "bigint" | "int8" => CastTarget::BigInt,
26723        "float8" | "double precision" => CastTarget::Float,
26724        "uuid" => CastTarget::Uuid,
26725        "bytea" => CastTarget::Bytea,
26726        "json" => CastTarget::Json,
26727        "jsonb" => CastTarget::Jsonb,
26728        // Types without a dedicated CastTarget variant flow through the
26729        // generic Named path (engine resolves via column_type_to_data_type).
26730        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26731        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26732        | "money" | "bit" | "varbit"
26733        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26734        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26735        // Range / multirange types likewise.
26736        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26737        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26738        | "datemultirange" | "tsmultirange" | "tstzmultirange"
26739        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26740        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26741            CastTarget::Named(alloc::string::String::from(ident))
26742        }
26743        _ => return None,
26744    })
26745}
26746
26747/// v7.12.4 — map a bare type-name identifier (the form that
26748/// appears in a function arg list or RETURNS clause) to a
26749/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26750/// types so the caller can preserve them as
26751/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26752///
26753/// Subset of the full column-type grammar — we deliberately
26754/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26755/// here because function-arg types in v7.12.4 are mostly the
26756/// bare form (`text`, `int`, `bytea`, …).
26757/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
26758/// than being `name TYPE`?
26759///
26760/// The multi-word spellings SQL allows for a bare argument type, each
26761/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
26762///
26763/// NOTE this list also exists in `spg-storage`, which computes the
26764/// signature key from the rendered argument text and has to reach the
26765/// same verdict. The two crates are siblings — neither depends on the
26766/// other — and each already carries its own table of type spellings
26767/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
26768/// there), so this follows the structure rather than inventing new
26769/// duplication. Recorded as V49.
26770pub fn is_multiword_type_phrase(phrase: &str) -> bool {
26771    let t = phrase.trim().to_ascii_lowercase();
26772    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
26773    matches!(
26774        base,
26775        "double precision"
26776            | "character varying"
26777            | "bit varying"
26778            | "timestamp with time zone"
26779            | "timestamp without time zone"
26780            | "time with time zone"
26781            | "time without time zone"
26782            | "national character"
26783            | "national character varying"
26784    )
26785}
26786
26787fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
26788    Some(match ident.to_ascii_lowercase().as_str() {
26789        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
26790        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
26791        "bigint" => ColumnTypeName::BigInt,
26792        "float" | "double" => ColumnTypeName::Float,
26793        // v7.39 (round 269) — real is 32-bit.
26794        "real" | "float4" => ColumnTypeName::Real,
26795        "text" => ColumnTypeName::Text,
26796        "bool" | "boolean" => ColumnTypeName::Bool,
26797        "date" => ColumnTypeName::Date,
26798        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
26799        "timestamptz" => ColumnTypeName::Timestamptz,
26800        "json" => ColumnTypeName::Json,
26801        "jsonb" => ColumnTypeName::Jsonb,
26802        "bytea" | "bytes" => ColumnTypeName::Bytes,
26803        "tsvector" => ColumnTypeName::TsVector,
26804        "tsquery" => ColumnTypeName::TsQuery,
26805        "uuid" => ColumnTypeName::Uuid,
26806        "interval" => ColumnTypeName::Interval,
26807        "time" => ColumnTypeName::Time,
26808        "year" => ColumnTypeName::Year,
26809        "timetz" => ColumnTypeName::TimeTz,
26810        "money" => ColumnTypeName::Money,
26811        _ => return None,
26812    })
26813}
26814
26815/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
26816/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
26817///
26818/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
26819/// / embedded SQL land in v7.12.5+):
26820///
26821/// ```text
26822///   body          := [ws] block [ws]
26823///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
26824///   stmt          := assign | return
26825///   assign        := assign_target := expr
26826///   assign_target := ( NEW | OLD ) . ident | ident
26827///   return        := RETURN ( NEW | OLD | NULL | expr )
26828/// ```
26829///
26830/// `expr` is parsed by recursing into the regular `Parser` — so a
26831/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
26832/// NEW.subject || ' ' || NEW.sender)` body shape works without
26833/// the body parser knowing what `to_tsvector` is.
26834///
26835/// Errors here cause the caller to fall back to
26836/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
26837/// successful, but the executor will refuse to invoke the
26838/// function with an "unparseable body" error.
26839/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
26840/// from the crate root as `spg_sql::parse_function_body`.
26841pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26842    parse_plpgsql_body(body)
26843}
26844
26845fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26846    // Use the regular lexer on the body text. The trailing
26847    // `END;` may or may not have a semicolon; the lexer treats
26848    // both forms identically.
26849    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
26850        message: alloc::format!("plpgsql body lex error: {e}"),
26851        token_pos: 0,
26852    })?;
26853    let mut parser = Parser::new(tokens);
26854    parser.parse_plpgsql_block()
26855}
26856
26857/// v7.39 (GUC) — the textual body of a SET value, for list joining.
26858fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
26859    match v {
26860        crate::ast::SetValue::String(s)
26861        | crate::ast::SetValue::Ident(s)
26862        | crate::ast::SetValue::Number(s) => s.clone(),
26863        crate::ast::SetValue::Default => "DEFAULT".into(),
26864    }
26865}
26866
26867/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
26868/// contains an aggregate call at ITS OWN query level (recursion stops at
26869/// sublink boundaries — a sublink's aggregates belong to the sublink).
26870/// Backs the "aggregate functions are not allowed in a recursive query's
26871/// recursive term" well-formedness check.
26872fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
26873    const AGG_NAMES: &[&str] = &[
26874        "count",
26875        "sum",
26876        "min",
26877        "max",
26878        "avg",
26879        "string_agg",
26880        "array_agg",
26881        "bool_and",
26882        "bool_or",
26883        "every",
26884        "any_value",
26885        "json_agg",
26886        "jsonb_agg",
26887        "json_object_agg",
26888        "jsonb_object_agg",
26889        "bit_and",
26890        "bit_or",
26891        "bit_xor",
26892        "var_pop",
26893        "var_samp",
26894        "variance",
26895        "stddev",
26896        "stddev_pop",
26897        "stddev_samp",
26898        "range_agg",
26899        "range_intersect_agg",
26900        "percentile_cont",
26901        "percentile_disc",
26902        "mode",
26903        "corr",
26904        "covar_pop",
26905        "covar_samp",
26906    ];
26907    match e {
26908        Expr::AggregateOrdered { .. } => true,
26909        Expr::FunctionCall { name, args } => {
26910            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
26911                || args.iter().any(expr_has_toplevel_aggregate)
26912        }
26913        Expr::NamedArg { expr, .. }
26914        | Expr::Variadic(expr)
26915        | Expr::Unary { expr, .. }
26916        | Expr::Cast { expr, .. }
26917        | Expr::IsNull { expr, .. }
26918        | Expr::FieldAccess { base: expr, .. }
26919        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
26920        Expr::Binary { lhs, rhs, .. } => {
26921            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
26922        }
26923        Expr::Like { expr, pattern, .. } => {
26924            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
26925        }
26926        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
26927        Expr::InList { expr, list, .. } => {
26928            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
26929        }
26930        Expr::ArraySubscript { target, index } => {
26931            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
26932        }
26933        Expr::ArraySlice { target, lo, hi } => {
26934            expr_has_toplevel_aggregate(target)
26935                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
26936                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
26937        }
26938        Expr::AnyAll { expr, array, .. } => {
26939            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
26940        }
26941        Expr::Case {
26942            operand,
26943            branches,
26944            else_branch,
26945        } => {
26946            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
26947                || branches
26948                    .iter()
26949                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
26950                || else_branch
26951                    .as_deref()
26952                    .is_some_and(expr_has_toplevel_aggregate)
26953        }
26954        // The outer-level operands of a sublink can aggregate; the sublink's
26955        // own body cannot leak its aggregates up here.
26956        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
26957        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
26958            row.iter().any(expr_has_toplevel_aggregate)
26959        }
26960        _ => false,
26961    }
26962}
26963
26964/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
26965/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
26966/// named table anywhere in its subtree. A plain FROM derived table is NOT a
26967/// sublink and is legal in a recursive term, so it is not walked here.
26968fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
26969    let mut exprs: Vec<&Expr> = Vec::new();
26970    for it in &s.items {
26971        if let crate::ast::SelectItem::Expr { expr, .. } = it {
26972            exprs.push(expr);
26973        }
26974    }
26975    if let Some(w) = &s.where_ {
26976        exprs.push(w);
26977    }
26978    if let Some(h) = &s.having {
26979        exprs.push(h);
26980    }
26981    if let Some(g) = &s.group_by {
26982        exprs.extend(g.iter());
26983    }
26984    if let Some(from) = &s.from {
26985        for j in &from.joins {
26986            if let Some(on) = &j.on {
26987                exprs.push(on);
26988            }
26989        }
26990    }
26991    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
26992}
26993
26994/// Does this expression contain a sublink whose subquery mentions `name`?
26995fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
26996    match e {
26997        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
26998        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
26999        Expr::InSubquery { expr, subquery, .. } => {
27000            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
27001        }
27002        Expr::RowInSubquery { row, subquery, .. } => {
27003            row.iter().any(|x| expr_sublink_mentions(x, name))
27004                || select_mentions_table(subquery, name)
27005        }
27006        Expr::RowCmpSubquery { row, subquery, .. } => {
27007            row.iter().any(|x| expr_sublink_mentions(x, name))
27008                || select_mentions_table(subquery, name)
27009        }
27010        Expr::NamedArg { expr, .. }
27011        | Expr::Variadic(expr)
27012        | Expr::Unary { expr, .. }
27013        | Expr::Cast { expr, .. }
27014        | Expr::IsNull { expr, .. }
27015        | Expr::FieldAccess { base: expr, .. }
27016        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
27017        Expr::Binary { lhs, rhs, .. } => {
27018            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
27019        }
27020        Expr::Like { expr, pattern, .. } => {
27021            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
27022        }
27023        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
27024            args.iter().any(|x| expr_sublink_mentions(x, name))
27025        }
27026        Expr::InList { expr, list, .. } => {
27027            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
27028        }
27029        Expr::ArraySubscript { target, index } => {
27030            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
27031        }
27032        Expr::ArraySlice { target, lo, hi } => {
27033            expr_sublink_mentions(target, name)
27034                || lo
27035                    .as_deref()
27036                    .is_some_and(|x| expr_sublink_mentions(x, name))
27037                || hi
27038                    .as_deref()
27039                    .is_some_and(|x| expr_sublink_mentions(x, name))
27040        }
27041        Expr::AnyAll { expr, array, .. } => {
27042            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
27043        }
27044        Expr::Case {
27045            operand,
27046            branches,
27047            else_branch,
27048        } => {
27049            operand
27050                .as_deref()
27051                .is_some_and(|x| expr_sublink_mentions(x, name))
27052                || branches
27053                    .iter()
27054                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
27055                || else_branch
27056                    .as_deref()
27057                    .is_some_and(|x| expr_sublink_mentions(x, name))
27058        }
27059        _ => false,
27060    }
27061}
27062
27063/// Does this SELECT (in full — FROM tables, derived tables, its own
27064/// sublinks, and union arms) mention the named table?
27065fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27066    if let Some(from) = &s.from {
27067        if from.primary.name.eq_ignore_ascii_case(name) {
27068            return true;
27069        }
27070        if let Some(sub) = &from.primary.lateral_subquery
27071            && select_mentions_table(sub, name)
27072        {
27073            return true;
27074        }
27075        for j in &from.joins {
27076            if j.table.name.eq_ignore_ascii_case(name) {
27077                return true;
27078            }
27079            if let Some(sub) = &j.table.lateral_subquery
27080                && select_mentions_table(sub, name)
27081            {
27082                return true;
27083            }
27084        }
27085    }
27086    if select_has_self_ref_in_sublink(s, name) {
27087        return true;
27088    }
27089    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27090}
27091
27092/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27093/// row count, the way PG evaluates one before applying it.
27094///
27095/// `None` = not a constant (a column, a subquery, a function call).
27096/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27097/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27098/// All wordings were read off live PG 18.4.
27099fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27100    use crate::ast::{BinOp, Expr, Literal, UnOp};
27101    match e {
27102        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27103        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27104            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27105        }
27106        // PG coerces a string by its CONTENT, and fails on the value.
27107        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27108            |_| {
27109                Err(alloc::format!(
27110                    "invalid input syntax for type bigint: \"{t}\""
27111                ))
27112            },
27113            |n| Ok(i128::from(n)),
27114        )),
27115        Expr::Literal(Literal::Bool(_)) => Some(Err(
27116            "argument of {L} must be type bigint, not type boolean".into(),
27117        )),
27118        Expr::Unary {
27119            op: UnOp::Neg,
27120            expr,
27121        } => match fold_limit_constant(expr)? {
27122            Ok(v) => Some(Ok(-v)),
27123            e @ Err(_) => Some(e),
27124        },
27125        Expr::Binary { lhs, op, rhs } => {
27126            let a = match fold_limit_constant(lhs)? {
27127                Ok(v) => v,
27128                e @ Err(_) => return Some(e),
27129            };
27130            let b = match fold_limit_constant(rhs)? {
27131                Ok(v) => v,
27132                e @ Err(_) => return Some(e),
27133            };
27134            let out = match op {
27135                BinOp::Add => a.checked_add(b),
27136                BinOp::Sub => a.checked_sub(b),
27137                BinOp::Mul => a.checked_mul(b),
27138                BinOp::Div if b != 0 => a.checked_div(b),
27139                BinOp::Div => return Some(Err("division by zero".into())),
27140                BinOp::Mod if b != 0 => a.checked_rem(b),
27141                BinOp::Mod => return Some(Err("division by zero".into())),
27142                _ => return None,
27143            };
27144            // PG evaluates the arithmetic in the operand's own type, so an
27145            // int-by-int product that leaves int range fails there — before
27146            // the row count is ever looked at.
27147            match out {
27148                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27149                    Some(Err("integer out of range".into()))
27150                }
27151                Some(v) => Some(Ok(v)),
27152                None => Some(Err("integer out of range".into())),
27153            }
27154        }
27155        _ => None,
27156    }
27157}
27158
27159/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27160/// cast, which is what makes `LIMIT 2.5` keep three rows.
27161fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27162    if scale == 0 {
27163        return unscaled;
27164    }
27165    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27166        return 0;
27167    };
27168    let neg = unscaled < 0;
27169    let mag = unscaled.unsigned_abs() as i128;
27170    let rounded = (mag + div / 2) / div;
27171    if neg { -rounded } else { rounded }
27172}
27173
27174#[cfg(test)]
27175mod tests {
27176    use super::*;
27177    use alloc::string::ToString;
27178
27179    fn parse(s: &str) -> Statement {
27180        parse_statement(s).expect("parse ok")
27181    }
27182
27183    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27184    // `tables`, `partition`, etc. are unreserved keywords per PG's
27185    // `pg_get_keywords()` and MUST be usable as column / table /
27186    // alias names. Pre-T4 every drop-in user whose schema had one
27187    // of these as a column name (sentori events.release, mailrs
27188    // messages.index in some forks) blew the parser up at CREATE
27189    // TABLE time with "expected identifier, got Release". The
27190    // generalisation lives in `unreserved_keyword_text` + the
27191    // `expect_ident_like` and `parse_atom` arms that consult it.
27192    #[test]
27193    fn release_usable_as_column_name_in_create_table() {
27194        let stmt =
27195            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27196        if let Statement::CreateTable(t) = stmt {
27197            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27198            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27199        } else {
27200            panic!("expected CreateTable");
27201        }
27202    }
27203
27204    #[test]
27205    fn release_usable_as_column_ref_in_select_projection() {
27206        // The sentori `0003_partition_events.sql` INSERT-SELECT
27207        // walk references `release` in both column lists; the
27208        // projection-side use exercises `parse_atom`'s relaxed
27209        // identifier set.
27210        parse("SELECT id, release, payload FROM events WHERE id = 1");
27211    }
27212
27213    #[test]
27214    fn release_usable_as_column_ref_in_insert_column_list() {
27215        // INSERT INTO t (id, release, payload) VALUES (…)
27216        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27217    }
27218
27219    #[test]
27220    fn alter_column_drop_not_null_uses_keyword_drop_token() {
27221        // Sentori `0013_audit_tombstone.sql` issues
27222        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27223        // emits Token::Drop (not Ident("drop")); the parser must
27224        // accept both in the ALTER COLUMN sub-dispatch.
27225        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27226    }
27227
27228    #[test]
27229    fn create_index_accepts_parenthesised_expression_key() {
27230        // sentori `0040_events_bundle_idx.sql` shape — JSONB
27231        // expression index. Pre-T4 the parser bailed at the
27232        // inner `(` with "expected column ident or expression,
27233        // got LParen". The Token::LParen arm in CREATE INDEX
27234        // routes through the expression parser instead.
27235        parse(
27236            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27237             ON events ((payload->'bundle'->>'id'))",
27238        );
27239    }
27240
27241    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27242    // surface as parse errors, never stack overflows (embed hosts
27243    // abort on overflow).
27244    /// The nesting budget is a COUNT; what it has to fit inside is a
27245    /// number of BYTES, and only one of those two is stable across
27246    /// compiler versions. Round 847 measured 30,336 bytes per level
27247    /// after a toolchain move, which puts 64 levels at 1.94 MB and
27248    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27249    /// aborted instead of erroring, which is precisely the outcome it
27250    /// exists to rule out.
27251    ///
27252    /// So the budget is metered rather than assumed. The ceiling leaves
27253    /// the depth SPG advertises fitting in a default 2 MiB thread with
27254    /// room to spare, in the debug build, where frames are widest.
27255    #[test]
27256    fn nesting_frame_cost_stays_under_ceiling() {
27257        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27258        // thread keeps a margin for whatever called the parser.
27259        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27260
27261        frame_meter::reset();
27262        let depth = frame_meter::SAMPLE_HI + 8;
27263        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27264        parse(&sql);
27265
27266        let per_level = frame_meter::bytes_per_level();
27267        {
27268            extern crate std;
27269            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27270        }
27271        assert!(
27272            per_level <= CEILING,
27273            "{per_level} bytes per nesting level exceeds {CEILING}; \
27274             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27275             in parse_expr_inner / parse_unary rather than lowering the \
27276             depth or widening the stack.",
27277            per_level * MAX_NEST_DEPTH
27278        );
27279    }
27280
27281    #[test]
27282    fn nesting_budget_errors_cleanly() {
27283        let depth = MAX_NEST_DEPTH + 50;
27284        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27285        let err = parse_statement(&sql).expect_err("must reject");
27286        assert!(err.message.contains("nests deeper"), "{err:?}");
27287        // Within budget still parses.
27288        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27289        parse(&sql);
27290    }
27291
27292    #[test]
27293    fn binary_chain_budget_errors_cleanly() {
27294        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27295        let err = parse_statement(&sql).expect_err("must reject");
27296        assert!(err.message.contains("chained binary"), "{err:?}");
27297        // Within budget still parses (chain depth ≤ budget is safe
27298        // for recursive eval/drop on 2 MiB stacks).
27299        let sql = format!("SELECT 1{}", " + 1".repeat(200));
27300        parse(&sql);
27301    }
27302
27303    #[test]
27304    fn in_list_unaffected_by_chain_budget() {
27305        // Flat InList: 20k elements parse fine and stay flat.
27306        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27307        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27308        let Statement::Select(s) = parse(&sql) else {
27309            panic!("expected select")
27310        };
27311        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27312            panic!("expected flat InList, got {:?}", s.where_)
27313        };
27314        assert_eq!(list.len(), 20_000);
27315        assert!(!negated);
27316    }
27317
27318    fn lit_int(n: i64) -> Expr {
27319        Expr::Literal(Literal::Integer(n))
27320    }
27321
27322    fn col(name: &str) -> Expr {
27323        Expr::Column(ColumnName {
27324            qualifier: None,
27325            name: name.into(),
27326        })
27327    }
27328
27329    #[test]
27330    fn select_single_integer() {
27331        let s = parse("SELECT 1");
27332        let Statement::Select(s) = s else {
27333            panic!("expected SELECT")
27334        };
27335        assert_eq!(s.items.len(), 1);
27336        assert!(s.from.is_none());
27337        assert!(s.where_.is_none());
27338    }
27339
27340    #[test]
27341    fn select_multiple_literal_kinds() {
27342        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27343        let Statement::Select(s) = s else {
27344            panic!("expected SELECT")
27345        };
27346        assert_eq!(s.items.len(), 5);
27347    }
27348
27349    #[test]
27350    fn select_wildcard_from_table() {
27351        let s = parse("SELECT * FROM users");
27352        let Statement::Select(s) = s else {
27353            panic!("expected SELECT")
27354        };
27355        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27356        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27357    }
27358
27359    #[test]
27360    fn select_with_table_alias() {
27361        let s = parse("SELECT * FROM users AS u");
27362        let Statement::Select(s) = s else {
27363            panic!("expected SELECT")
27364        };
27365        let t = &s.from.as_ref().unwrap().primary;
27366        assert_eq!(t.name, "users");
27367        assert_eq!(t.alias.as_deref(), Some("u"));
27368    }
27369
27370    #[test]
27371    fn select_with_where_eq() {
27372        let s = parse("SELECT a FROM t WHERE a = 1");
27373        let Statement::Select(s) = s else {
27374            panic!("expected SELECT")
27375        };
27376        let w = s.where_.unwrap();
27377        assert_eq!(
27378            w,
27379            Expr::Binary {
27380                lhs: Box::new(col("a")),
27381                op: BinOp::Eq,
27382                rhs: Box::new(lit_int(1)),
27383            }
27384        );
27385    }
27386
27387    #[test]
27388    fn arithmetic_precedence() {
27389        let s = parse("SELECT 1 + 2 * 3");
27390        let Statement::Select(s) = s else {
27391            panic!("expected SELECT")
27392        };
27393        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27394            panic!("wildcard?")
27395        };
27396        assert_eq!(
27397            expr,
27398            &Expr::Binary {
27399                lhs: Box::new(lit_int(1)),
27400                op: BinOp::Add,
27401                rhs: Box::new(Expr::Binary {
27402                    lhs: Box::new(lit_int(2)),
27403                    op: BinOp::Mul,
27404                    rhs: Box::new(lit_int(3)),
27405                }),
27406            }
27407        );
27408    }
27409
27410    #[test]
27411    fn parentheses_override_precedence() {
27412        let s = parse("SELECT (1 + 2) * 3");
27413        let Statement::Select(s) = s else {
27414            panic!("expected SELECT")
27415        };
27416        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27417            panic!()
27418        };
27419        assert_eq!(
27420            expr,
27421            &Expr::Binary {
27422                lhs: Box::new(Expr::Binary {
27423                    lhs: Box::new(lit_int(1)),
27424                    op: BinOp::Add,
27425                    rhs: Box::new(lit_int(2)),
27426                }),
27427                op: BinOp::Mul,
27428                rhs: Box::new(lit_int(3)),
27429            }
27430        );
27431    }
27432
27433    #[test]
27434    fn not_binds_below_comparison() {
27435        // `NOT a = 1` should parse as `NOT (a = 1)`.
27436        let s = parse("SELECT NOT a = 1 FROM t");
27437        let Statement::Select(s) = s else {
27438            panic!("expected SELECT")
27439        };
27440        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27441            panic!()
27442        };
27443        assert_eq!(
27444            expr,
27445            &Expr::Unary {
27446                op: UnOp::Not,
27447                expr: Box::new(Expr::Binary {
27448                    lhs: Box::new(col("a")),
27449                    op: BinOp::Eq,
27450                    rhs: Box::new(lit_int(1)),
27451                }),
27452            }
27453        );
27454    }
27455
27456    #[test]
27457    fn unary_minus_binds_above_multiplication() {
27458        // `-a * 2` should be `(-a) * 2`.
27459        let s = parse("SELECT -a * 2 FROM t");
27460        let Statement::Select(s) = s else {
27461            panic!("expected SELECT")
27462        };
27463        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27464            panic!()
27465        };
27466        assert_eq!(
27467            expr,
27468            &Expr::Binary {
27469                lhs: Box::new(Expr::Unary {
27470                    op: UnOp::Neg,
27471                    expr: Box::new(col("a")),
27472                }),
27473                op: BinOp::Mul,
27474                rhs: Box::new(lit_int(2)),
27475            }
27476        );
27477    }
27478
27479    #[test]
27480    fn qualified_column() {
27481        let s = parse("SELECT t.col FROM t");
27482        let Statement::Select(s) = s else {
27483            panic!("expected SELECT")
27484        };
27485        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27486            panic!()
27487        };
27488        assert_eq!(
27489            expr,
27490            &Expr::Column(ColumnName {
27491                qualifier: Some("t".into()),
27492                name: "col".into()
27493            })
27494        );
27495    }
27496
27497    #[test]
27498    fn select_item_alias_with_as() {
27499        let s = parse("SELECT a AS y FROM t");
27500        let Statement::Select(s) = s else {
27501            panic!("expected SELECT")
27502        };
27503        let SelectItem::Expr { alias, .. } = &s.items[0] else {
27504            panic!()
27505        };
27506        assert_eq!(alias.as_deref(), Some("y"));
27507    }
27508
27509    #[test]
27510    fn trailing_semicolon_accepted() {
27511        let s = parse("SELECT 1;");
27512        let Statement::Select(s) = s else {
27513            panic!("expected SELECT")
27514        };
27515        assert_eq!(s.items.len(), 1);
27516    }
27517
27518    #[test]
27519    fn boolean_chain_with_and_or_not() {
27520        // (NOT a) OR (b AND (NOT c))
27521        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27522        let Statement::Select(s) = s else {
27523            panic!("expected SELECT")
27524        };
27525        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27526            panic!()
27527        };
27528        let expected = Expr::Binary {
27529            lhs: Box::new(Expr::Unary {
27530                op: UnOp::Not,
27531                expr: Box::new(col("a")),
27532            }),
27533            op: BinOp::Or,
27534            rhs: Box::new(Expr::Binary {
27535                lhs: Box::new(col("b")),
27536                op: BinOp::And,
27537                rhs: Box::new(Expr::Unary {
27538                    op: UnOp::Not,
27539                    expr: Box::new(col("c")),
27540                }),
27541            }),
27542        };
27543        assert_eq!(expr, &expected);
27544    }
27545
27546    #[test]
27547    fn empty_input_errors() {
27548        // v7.14.0 — pg_dump preambles emit several comment-only
27549        // / blank-line statements that collapse to Statement::
27550        // Empty rather than a parse error. The old "SELECT in
27551        // message" assertion is stale; verify the new contract:
27552        // empty / whitespace / comment-only input parses to
27553        // Statement::Empty.
27554        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27555        assert!(matches!(
27556            parse_statement("  \n\t ").unwrap(),
27557            Statement::Empty
27558        ));
27559        // Sanity: malformed-but-non-empty still errors.
27560        assert!(parse_statement("SELECT FROM WHERE").is_err());
27561    }
27562
27563    #[test]
27564    fn unmatched_paren_errors() {
27565        assert!(parse_statement("SELECT (1 + 2").is_err());
27566    }
27567
27568    #[test]
27569    fn display_round_trip_simple_select() {
27570        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27571        let text = original.to_string();
27572        let again = parse_statement(&text).expect("re-parse");
27573        assert_eq!(original, again);
27574    }
27575
27576    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27577
27578    #[test]
27579    fn create_table_single_column() {
27580        let s = parse("CREATE TABLE foo (a INT)");
27581        let Statement::CreateTable(c) = s else {
27582            panic!("expected CreateTable")
27583        };
27584        assert_eq!(c.name, "foo");
27585        assert_eq!(c.columns.len(), 1);
27586        assert_eq!(c.columns[0].name, "a");
27587        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27588        assert!(c.columns[0].nullable);
27589    }
27590
27591    #[test]
27592    fn create_table_multi_column_with_not_null_mix() {
27593        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27594        let Statement::CreateTable(c) = s else {
27595            panic!()
27596        };
27597        assert_eq!(c.columns.len(), 4);
27598        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27599        assert!(!c.columns[0].nullable);
27600        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27601        assert!(c.columns[1].nullable);
27602        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27603        assert!(!c.columns[2].nullable);
27604        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27605    }
27606
27607    #[test]
27608    fn create_table_bigint_supported() {
27609        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27610        let Statement::CreateTable(c) = s else {
27611            panic!()
27612        };
27613        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27614    }
27615
27616    #[test]
27617    fn create_table_vector_default_is_f32() {
27618        let s = parse("CREATE TABLE t (v VECTOR(128))");
27619        let Statement::CreateTable(c) = s else {
27620            panic!()
27621        };
27622        assert_eq!(
27623            c.columns[0].ty,
27624            ColumnTypeName::Vector {
27625                dim: 128,
27626                encoding: VecEncoding::F32,
27627            },
27628        );
27629    }
27630
27631    #[test]
27632    fn create_table_vector_using_sq8() {
27633        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27634        // Case-insensitive on both `USING` and the encoding name.
27635        for sql in [
27636            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27637            "CREATE TABLE t (v VECTOR(128) using sq8)",
27638        ] {
27639            let s = parse(sql);
27640            let Statement::CreateTable(c) = s else {
27641                panic!()
27642            };
27643            assert_eq!(
27644                c.columns[0].ty,
27645                ColumnTypeName::Vector {
27646                    dim: 128,
27647                    encoding: VecEncoding::Sq8,
27648                },
27649                "{sql}",
27650            );
27651        }
27652    }
27653
27654    #[test]
27655    fn create_table_vector_using_unknown_errors() {
27656        // v7.16.1 — the inline `USING <encoding>` shape on
27657        // CREATE TABLE column defs was withdrawn before
27658        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27659        // (col vector_<metric>_ops)`; the parser now rejects
27660        // USING at column-list position with a clearer
27661        // "expected ',' or ')'" message. Test asserts the
27662        // current rejection, not the old "unknown vector
27663        // encoding" string.
27664        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27665        assert!(
27666            err.message.contains("USING")
27667                || err.message.contains("using")
27668                || err.message.contains("')'")
27669                || err.message.contains("','"),
27670            "expected USING/column-list rejection, got: {}",
27671            err.message
27672        );
27673    }
27674
27675    #[test]
27676    fn vector_using_sq8_display_roundtrips() {
27677        // The Display impl must produce text that re-parses to the
27678        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27679        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27680        let Statement::CreateTable(c) = s else {
27681            panic!()
27682        };
27683        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27684    }
27685
27686    #[test]
27687    fn parser_recognises_placeholders() {
27688        use crate::ast::{Expr, SelectItem, Statement};
27689        // $N in expression position parses as Expr::Placeholder(N).
27690        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27691        let Statement::Select(sel) = s else { panic!() };
27692        assert!(matches!(
27693            sel.items[0],
27694            SelectItem::Expr {
27695                expr: Expr::Placeholder(1),
27696                alias: None
27697            }
27698        ));
27699        // $2 + 1
27700        let SelectItem::Expr {
27701            expr: Expr::Binary { lhs, rhs, .. },
27702            ..
27703        } = &sel.items[1]
27704        else {
27705            panic!()
27706        };
27707        assert!(matches!(**lhs, Expr::Placeholder(2)));
27708        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27709        // WHERE x = $3
27710        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27711            panic!()
27712        };
27713        assert!(matches!(**rhs, Expr::Placeholder(3)));
27714    }
27715
27716    #[test]
27717    fn parser_rejects_dollar_zero() {
27718        // $0 is not valid in PG; the lexer rejects it.
27719        assert!(parse_statement("SELECT $0").is_err());
27720    }
27721
27722    #[test]
27723    fn placeholder_display_roundtrips() {
27724        // The Display impl must produce text that re-lexes to the
27725        // same Placeholder token.
27726        let s = parse("SELECT $42 FROM t");
27727        let printed = s.to_string();
27728        assert!(printed.contains("$42"));
27729        let again = parse(&printed);
27730        assert_eq!(s, again);
27731    }
27732
27733    #[test]
27734    fn alter_index_rebuild_bare() {
27735        use crate::ast::{AlterIndexTarget, Statement};
27736        let s = parse("ALTER INDEX my_idx REBUILD");
27737        let Statement::AlterIndex(a) = s else {
27738            panic!("expected AlterIndex, got {s:?}")
27739        };
27740        assert_eq!(a.name, "my_idx");
27741        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27742    }
27743
27744    #[test]
27745    fn alter_index_rebuild_with_encoding() {
27746        use crate::ast::{AlterIndexTarget, Statement};
27747        for (sql, want) in [
27748            (
27749                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27750                VecEncoding::F32,
27751            ),
27752            (
27753                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27754                VecEncoding::Sq8,
27755            ),
27756            (
27757                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27758                VecEncoding::F16,
27759            ),
27760        ] {
27761            let s = parse(sql);
27762            let Statement::AlterIndex(a) = s else {
27763                panic!("{sql}: expected AlterIndex")
27764            };
27765            assert_eq!(a.name, "my_idx");
27766            assert_eq!(
27767                a.target,
27768                AlterIndexTarget::Rebuild {
27769                    encoding: Some(want)
27770                },
27771                "{sql}"
27772            );
27773        }
27774    }
27775
27776    #[test]
27777    fn alter_index_rebuild_unknown_encoding_errors() {
27778        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
27779        assert!(
27780            err.message.contains("unknown vector encoding"),
27781            "got: {}",
27782            err.message
27783        );
27784    }
27785
27786    #[test]
27787    fn alter_index_rebuild_display_roundtrips() {
27788        for (input, want) in [
27789            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
27790            (
27791                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27792                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27793            ),
27794            (
27795                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27796                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27797            ),
27798        ] {
27799            let s = parse(input);
27800            assert_eq!(s.to_string(), want);
27801        }
27802    }
27803
27804    #[test]
27805    fn create_table_unknown_type_defers_to_engine() {
27806        // v4.9 picked XML as a parse-time "unsupported column
27807        // type" probe. v7.17.0 Phase 1.4 changed the contract:
27808        // an unknown type ident parses as Text + `user_type_ref`
27809        // so CREATE TABLE can resolve user-defined enum / domain
27810        // types — rejection of truly-unknown types moved to the
27811        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
27812        // to a first-class built-in, so this probe switched to a
27813        // synthetic name nothing in the lexer will ever recognise.
27814        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
27815        let Statement::CreateTable(t) = stmt else {
27816            panic!("expected CreateTable");
27817        };
27818        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
27819    }
27820
27821    #[test]
27822    fn create_table_missing_table_keyword_errors() {
27823        assert!(parse_statement("CREATE x (a INT)").is_err());
27824    }
27825
27826    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
27827    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
27828
27829    #[test]
27830    fn parse_create_table_partition_by_range() {
27831        use crate::ast::{PartitionBySpec, PartitionKindAst};
27832        let stmt = parse_statement(
27833            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
27834             payload JSONB) PARTITION BY RANGE (ts)",
27835        )
27836        .unwrap();
27837        let Statement::CreateTable(t) = stmt else {
27838            panic!("expected CreateTable");
27839        };
27840        assert!(t.partition_of.is_none(), "parent has no partition_of");
27841        assert_eq!(t.columns.len(), 3);
27842        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
27843        assert_eq!(
27844            by,
27845            &PartitionBySpec {
27846                kind: PartitionKindAst::Range,
27847                key_columns: alloc::vec!["ts".to_string()],
27848            }
27849        );
27850        // Display round-trip preserves the suffix. `quote_ident`
27851        // only adds double quotes when the ident needs escaping, so
27852        // a plain `ts` survives bare here.
27853        assert!(
27854            t.to_string().contains("PARTITION BY RANGE (ts)"),
27855            "Display lost PARTITION BY suffix: {t}"
27856        );
27857    }
27858
27859    #[test]
27860    fn parse_create_table_partition_of_range() {
27861        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
27862        let stmt = parse_statement(
27863            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
27864             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
27865        )
27866        .unwrap();
27867        let Statement::CreateTable(t) = stmt else {
27868            panic!("expected CreateTable");
27869        };
27870        assert!(t.columns.is_empty(), "child inherits columns from parent");
27871        assert!(t.partition_by.is_none());
27872        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27873        assert_eq!(of.parent_name, "events_partitioned");
27874        let PartitionOfSpec { bounds, .. } = of.clone();
27875        match bounds {
27876            PartitionOfBoundsAst::Range { lower, upper } => {
27877                assert!(lower.to_string().contains("2026-06-01"));
27878                assert!(upper.to_string().contains("2026-07-01"));
27879            }
27880            other => panic!("expected Range, got {other:?}"),
27881        }
27882        // Display round-trip emits the FOR VALUES tail. `quote_ident`
27883        // skips quotes when not required, so the parent name appears
27884        // bare here.
27885        let s = t.to_string();
27886        assert!(
27887            s.contains("PARTITION OF events_partitioned"),
27888            "Display lost PARTITION OF: {s}"
27889        );
27890        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
27891        assert!(s.contains(") TO ("), "Display lost TO: {s}");
27892    }
27893
27894    #[test]
27895    fn parse_create_table_partition_of_default() {
27896        use crate::ast::PartitionOfBoundsAst;
27897        let stmt =
27898            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
27899                .unwrap();
27900        let Statement::CreateTable(t) = stmt else {
27901            panic!("expected CreateTable");
27902        };
27903        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27904        assert_eq!(of.parent_name, "events_partitioned");
27905        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
27906        assert!(
27907            t.to_string()
27908                .contains("PARTITION OF events_partitioned DEFAULT"),
27909            "Display lost DEFAULT: {t}"
27910        );
27911    }
27912
27913    #[test]
27914    fn parse_create_table_partition_by_list() {
27915        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
27916        // child with `FOR VALUES IN (lit, lit, …)`.
27917        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27918        let parent =
27919            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
27920                .unwrap();
27921        let Statement::CreateTable(t) = parent else {
27922            panic!("expected CreateTable");
27923        };
27924        let Some(PartitionBySpec {
27925            kind,
27926            ref key_columns,
27927        }) = t.partition_by
27928        else {
27929            panic!("expected PARTITION BY");
27930        };
27931        assert_eq!(kind, PartitionKindAst::List);
27932        assert_eq!(*key_columns, vec!["region".to_string()]);
27933        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
27934
27935        let child = parse_statement(
27936            "CREATE TABLE events_apac PARTITION OF events_listed \
27937             FOR VALUES IN ('jp', 'kr', 'tw')",
27938        )
27939        .unwrap();
27940        let Statement::CreateTable(c) = child else {
27941            panic!("expected CreateTable");
27942        };
27943        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27944        let PartitionOfBoundsAst::List { values } = &of.bounds else {
27945            panic!("expected List bounds, got {:?}", of.bounds);
27946        };
27947        assert_eq!(values.len(), 3);
27948        let disp = c.to_string();
27949        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
27950    }
27951
27952    #[test]
27953    fn parse_create_table_partition_by_hash() {
27954        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
27955        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
27956        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27957        let parent =
27958            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
27959        let Statement::CreateTable(t) = parent else {
27960            panic!("expected CreateTable");
27961        };
27962        let Some(PartitionBySpec {
27963            kind,
27964            ref key_columns,
27965        }) = t.partition_by
27966        else {
27967            panic!("expected PARTITION BY");
27968        };
27969        assert_eq!(kind, PartitionKindAst::Hash);
27970        assert_eq!(*key_columns, vec!["id".to_string()]);
27971        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
27972
27973        let child = parse_statement(
27974            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
27975             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
27976        )
27977        .unwrap();
27978        let Statement::CreateTable(c) = child else {
27979            panic!("expected CreateTable");
27980        };
27981        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27982        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
27983            panic!("expected Hash bounds");
27984        };
27985        assert_eq!(modulus, 4);
27986        assert_eq!(remainder, 0);
27987        let disp = c.to_string();
27988        assert!(
27989            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
27990            "Display lost HASH bounds: {disp}"
27991        );
27992
27993        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
27994        let bad = parse_statement(
27995            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
27996             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
27997        );
27998        let msg = format!("{}", bad.unwrap_err());
27999        assert!(
28000            msg.contains("REMAINDER") && msg.contains("MODULUS"),
28001            "expected REMAINDER/MODULUS validation error: {msg}"
28002        );
28003    }
28004
28005    #[test]
28006    fn parse_create_table_partition_of_rejects_columns() {
28007        // v7.37.6-B contract: PARTITION OF children inherit columns
28008        // from the parent; an explicit list MUST surface as a parse
28009        // error rather than getting silently ignored.
28010        let err = parse_statement(
28011            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
28012             FOR VALUES FROM ('a') TO ('b')",
28013        );
28014        assert!(err.is_err(), "expected parse error for explicit columns");
28015        let msg = format!("{}", err.unwrap_err());
28016        assert!(
28017            msg.contains("PARTITION OF") && msg.contains("column"),
28018            "error should mention PARTITION OF + columns: {msg}"
28019        );
28020    }
28021
28022    #[test]
28023    fn insert_single_value() {
28024        let s = parse("INSERT INTO foo VALUES (42)");
28025        let Statement::Insert(i) = s else {
28026            panic!("expected Insert")
28027        };
28028        assert_eq!(i.table, "foo");
28029        assert_eq!(i.rows.len(), 1);
28030        assert_eq!(i.rows[0].len(), 1);
28031        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
28032    }
28033
28034    #[test]
28035    fn insert_multi_value_with_mixed_literals() {
28036        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
28037        let Statement::Insert(i) = s else { panic!() };
28038        assert_eq!(i.rows.len(), 1);
28039        assert_eq!(i.rows[0].len(), 5);
28040    }
28041
28042    #[test]
28043    fn insert_missing_into_errors() {
28044        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
28045    }
28046
28047    #[test]
28048    fn create_table_round_trip() {
28049        let original =
28050            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
28051        let text = original.to_string();
28052        let again = parse_statement(&text).expect("re-parse");
28053        assert_eq!(original, again);
28054    }
28055
28056    #[test]
28057    fn insert_round_trip_with_negation_and_string() {
28058        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28059        let text = original.to_string();
28060        let again = parse_statement(&text).expect("re-parse");
28061        assert_eq!(original, again);
28062    }
28063
28064    #[test]
28065    fn unknown_keyword_at_statement_start_errors() {
28066        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28067        // the top-level dispatch still has no branch to take.
28068        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28069        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28070    }
28071
28072    // --- v0.8 CREATE INDEX --------------------------------------------------
28073
28074    #[test]
28075    fn create_index_basic() {
28076        let s = parse("CREATE INDEX idx_id ON users (id)");
28077        let Statement::CreateIndex(c) = s else {
28078            panic!("expected CreateIndex")
28079        };
28080        assert_eq!(c.name, "idx_id");
28081        assert_eq!(c.table, "users");
28082        assert_eq!(c.column, "id");
28083    }
28084
28085    #[test]
28086    fn create_index_missing_on_errors() {
28087        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28088    }
28089
28090    #[test]
28091    fn create_index_missing_paren_errors() {
28092        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28093    }
28094
28095    #[test]
28096    fn create_index_round_trip() {
28097        let original = parse("CREATE INDEX by_name ON users (name)");
28098        let again = parse_statement(&original.to_string()).unwrap();
28099        assert_eq!(original, again);
28100    }
28101
28102    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28103
28104    #[test]
28105    fn create_unique_index_basic() {
28106        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28107        let Statement::CreateIndex(c) = s else {
28108            panic!("expected CreateIndex");
28109        };
28110        assert!(c.is_unique);
28111        assert_eq!(c.column, "a");
28112        assert!(c.partial_predicate.is_none());
28113    }
28114
28115    #[test]
28116    fn create_unique_index_partial() {
28117        // mailrs's email_templates "one default per user" shape.
28118        let s = parse(
28119            "CREATE UNIQUE INDEX idx_email_templates_user_default \
28120             ON email_templates (user_address) WHERE is_default = true",
28121        );
28122        let Statement::CreateIndex(c) = s else {
28123            panic!("expected CreateIndex");
28124        };
28125        assert!(c.is_unique);
28126        assert_eq!(c.table, "email_templates");
28127        assert_eq!(c.column, "user_address");
28128        assert!(c.partial_predicate.is_some());
28129    }
28130
28131    #[test]
28132    fn create_unique_index_composite_with_predicate() {
28133        // mailrs's calendar_events instance: composite columns.
28134        let s = parse(
28135            "CREATE UNIQUE INDEX uq_calendar_events_instance \
28136             ON calendar_events (calendar_id, uid, recurrence_id) \
28137             WHERE recurrence_id IS NOT NULL",
28138        );
28139        let Statement::CreateIndex(c) = s else {
28140            panic!("expected CreateIndex");
28141        };
28142        assert!(c.is_unique);
28143        assert_eq!(c.column, "calendar_id");
28144        assert_eq!(
28145            c.extra_columns,
28146            vec!["uid".to_string(), "recurrence_id".to_string()]
28147        );
28148        assert!(c.partial_predicate.is_some());
28149    }
28150
28151    #[test]
28152    fn create_unique_index_using_btree_ok() {
28153        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28154        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28155    }
28156
28157    #[test]
28158    fn create_unique_index_using_hnsw_rejected() {
28159        let err =
28160            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28161        assert!(err.message.contains("UNIQUE"), "{}", err.message);
28162    }
28163
28164    #[test]
28165    fn create_unique_index_round_trip() {
28166        let original = parse(
28167            "CREATE UNIQUE INDEX uq_calendar_events_master \
28168             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28169        );
28170        let again = parse_statement(&original.to_string()).unwrap();
28171        assert_eq!(original, again);
28172    }
28173
28174    #[test]
28175    fn create_unique_without_index_errors() {
28176        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28177        // v7.39 (round 340, V56) — PG 18.4, verbatim.
28178        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28179    }
28180
28181    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28182
28183    #[test]
28184    fn create_table_bytea_column() {
28185        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28186        let Statement::CreateTable(c) = s else {
28187            panic!("expected CreateTable");
28188        };
28189        assert_eq!(c.columns.len(), 2);
28190        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28191        assert!(!c.columns[1].nullable);
28192    }
28193
28194    #[test]
28195    fn create_table_bytes_alias_column() {
28196        let s = parse("CREATE TABLE t (blob BYTES)");
28197        let Statement::CreateTable(c) = s else {
28198            panic!("expected CreateTable");
28199        };
28200        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28201    }
28202
28203    #[test]
28204    fn bytea_round_trip_display() {
28205        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28206        let again = parse_statement(&original.to_string()).unwrap();
28207        assert_eq!(original, again);
28208    }
28209
28210    // --- v0.9 transactions -------------------------------------------------
28211
28212    #[test]
28213    fn begin_commit_rollback_parse_as_unit_variants() {
28214        assert_eq!(parse("BEGIN"), Statement::Begin(None));
28215        assert_eq!(parse("COMMIT"), Statement::Commit);
28216        // r1066 — PG synonyms pgbench's tpcb script relies on.
28217        assert_eq!(parse("END"), Statement::Commit);
28218        assert_eq!(parse("END TRANSACTION"), Statement::Commit);
28219        assert_eq!(parse("COMMIT WORK"), Statement::Commit);
28220        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28221        // Trailing semicolons accepted too.
28222        assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28223        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28224        // statement (with or without the WORK/TRANSACTION noise word).
28225        assert_eq!(
28226            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28227            Statement::Begin(Some(IsolationLevel::RepeatableRead))
28228        );
28229        assert_eq!(
28230            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28231            Statement::Begin(Some(IsolationLevel::Serializable))
28232        );
28233        // A non-isolation mode keeps the session default (None).
28234        assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28235    }
28236
28237    // --- v1.2: pgvector distance ops + ::vector cast --------------------
28238
28239    #[test]
28240    fn inner_product_binop_parses() {
28241        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28242        let Statement::Select(s) = s else { panic!() };
28243        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28244            panic!()
28245        };
28246        assert!(matches!(
28247            expr,
28248            Expr::Binary {
28249                op: BinOp::InnerProduct,
28250                ..
28251            }
28252        ));
28253    }
28254
28255    #[test]
28256    fn cosine_distance_binop_parses() {
28257        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28258        let Statement::Select(s) = s else { panic!() };
28259        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28260            panic!()
28261        };
28262        assert!(matches!(
28263            expr,
28264            Expr::Binary {
28265                op: BinOp::CosineDistance,
28266                ..
28267            }
28268        ));
28269    }
28270
28271    #[test]
28272    fn vector_cast_postfix_wraps_string_literal() {
28273        let s = parse("SELECT '[1,2,3]'::vector FROM t");
28274        let Statement::Select(s) = s else { panic!() };
28275        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28276            panic!()
28277        };
28278        assert!(matches!(
28279            expr,
28280            Expr::Cast {
28281                target: CastTarget::Vector,
28282                ..
28283            }
28284        ));
28285    }
28286
28287    #[test]
28288    fn unsupported_cast_target_errors() {
28289        // v7.37.5 ship triage promoted the parser to accept every
28290        // ident as a `CastTarget::Named(canonical)`; the engine
28291        // surfaces the "unsupported cast target" error at eval
28292        // time when `type_name_to_data_type` can't resolve it.
28293        // Parser-side error now requires a NON-ident after `::`
28294        // (e.g. a punctuation token).
28295        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28296        assert_eq!(err.message, "syntax error at or near \",\"");
28297    }
28298
28299    #[test]
28300    fn tx_statements_round_trip() {
28301        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28302            let original = parse(q);
28303            let again = parse_statement(&original.to_string()).unwrap();
28304            assert_eq!(original, again);
28305        }
28306    }
28307
28308    #[test]
28309    fn interval_text_parsing_units() {
28310        // v7.37.5 β — three-field shape `(months, days, micros)` so
28311        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28312        // Single unit.
28313        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28314        assert_eq!(
28315            parse_interval_text("24 hours"),
28316            Some((0, 0, 86_400_000_000))
28317        );
28318        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28319        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28320        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28321        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28322        // Compound spans accumulate per-dimension.
28323        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28324        assert_eq!(
28325            parse_interval_text("1 day 2 hours"),
28326            Some((0, 1, 7_200_000_000))
28327        );
28328        // Negative numbers carry through per-dimension.
28329        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28330        // Bad shapes return None.
28331        assert_eq!(parse_interval_text(""), None);
28332        assert_eq!(parse_interval_text("garbage"), None);
28333        assert_eq!(parse_interval_text("1 fortnight"), None);
28334        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28335        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28336        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28337        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28338        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28339    }
28340
28341    #[test]
28342    fn interval_literal_roundtrips_via_display() {
28343        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28344        let s = parsed.to_string();
28345        // Display preserves the original text verbatim.
28346        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28347        // And re-parsing yields a structurally equal statement.
28348        let again = parse_statement(&s).unwrap();
28349        assert_eq!(parsed, again);
28350    }
28351
28352    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28353
28354    #[test]
28355    fn parser_recognises_create_publication_bare() {
28356        let s = parse("CREATE PUBLICATION pub_a");
28357        let Statement::CreatePublication(p) = s else {
28358            panic!("expected CreatePublication, got {s:?}")
28359        };
28360        assert_eq!(p.name, "pub_a");
28361        assert_eq!(p.scope, PublicationScope::AllTables);
28362    }
28363
28364    #[test]
28365    fn parser_recognises_create_publication_for_all_tables() {
28366        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28367        let Statement::CreatePublication(p) = s else {
28368            panic!("expected CreatePublication, got {s:?}")
28369        };
28370        assert_eq!(p.name, "pub_a");
28371        assert_eq!(p.scope, PublicationScope::AllTables);
28372    }
28373
28374    #[test]
28375    fn parser_recognises_drop_publication() {
28376        let s = parse("DROP PUBLICATION pub_a");
28377        let Statement::DropPublication { name, .. } = s else {
28378            panic!("expected DropPublication, got {s:?}")
28379        };
28380        assert_eq!(name, "pub_a");
28381    }
28382
28383    #[test]
28384    fn parser_recognises_for_table_list() {
28385        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28386        let Statement::CreatePublication(p) = s else {
28387            panic!("expected CreatePublication, got {s:?}")
28388        };
28389        assert_eq!(p.name, "pub_a");
28390        let PublicationScope::ForTables(ts) = p.scope else {
28391            panic!("expected ForTables scope")
28392        };
28393        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28394    }
28395
28396    #[test]
28397    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28398        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28399        // is rejected (`invalid publication object list`; the old
28400        // test pinned an unverifiable "PG 19 accepts both" claim);
28401        // TABLES pairs with IN SCHEMA.
28402        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28403            .expect_err("bare FOR TABLES must reject");
28404        assert!(
28405            alloc::format!("{err}").contains("invalid publication object list"),
28406            "got: {err}"
28407        );
28408        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28409        let Statement::CreatePublication(p) = s else {
28410            panic!("expected CreatePublication, got {s:?}")
28411        };
28412        let PublicationScope::TablesInSchema(schema) = p.scope else {
28413            panic!("expected TablesInSchema")
28414        };
28415        assert_eq!(schema, "public");
28416    }
28417
28418    #[test]
28419    fn parser_recognises_for_all_tables_except_list() {
28420        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28421        let Statement::CreatePublication(p) = s else {
28422            panic!()
28423        };
28424        let PublicationScope::AllTablesExcept(ts) = p.scope else {
28425            panic!("expected AllTablesExcept")
28426        };
28427        assert_eq!(ts, alloc::vec!["t1", "t2"]);
28428    }
28429
28430    #[test]
28431    fn parser_rejects_for_table_with_empty_list() {
28432        // `FOR TABLE` with nothing after is a parse error.
28433        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28434            .expect_err("must error on empty list");
28435        // No specific message asserted — the call falls through to
28436        // expect_ident_like which yields "expected identifier, got …".
28437        assert!(!err.message.is_empty());
28438    }
28439
28440    #[test]
28441    fn parser_recognises_show_publications() {
28442        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28443        // bare ident in this position, NOT a reserved keyword.
28444        let s = parse("SHOW PUBLICATIONS");
28445        assert!(matches!(s, Statement::ShowPublications));
28446    }
28447
28448    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28449
28450    #[test]
28451    fn parser_recognises_create_subscription_single_publication() {
28452        let s = parse(
28453            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28454        );
28455        let Statement::CreateSubscription(c) = s else {
28456            panic!("expected CreateSubscription, got {s:?}")
28457        };
28458        assert_eq!(c.name, "sub_a");
28459        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28460        assert_eq!(c.publications, alloc::vec!["pub_a"]);
28461    }
28462
28463    #[test]
28464    fn parser_recognises_create_subscription_multi_publication() {
28465        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28466        let Statement::CreateSubscription(c) = s else {
28467            panic!()
28468        };
28469        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28470    }
28471
28472    #[test]
28473    fn parser_rejects_create_subscription_missing_connection() {
28474        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28475            .expect_err("must error on missing CONNECTION");
28476        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28477    }
28478
28479    #[test]
28480    fn parser_rejects_create_subscription_missing_publication() {
28481        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28482            .expect_err("must error on missing PUBLICATION");
28483        assert_eq!(err.message, "syntax error at end of input");
28484    }
28485
28486    #[test]
28487    fn parser_recognises_drop_subscription() {
28488        let s = parse("DROP SUBSCRIPTION sub_a");
28489        let Statement::DropSubscription { name, .. } = s else {
28490            panic!("expected DropSubscription, got {s:?}")
28491        };
28492        assert_eq!(name, "sub_a");
28493    }
28494
28495    #[test]
28496    fn parser_recognises_show_subscriptions() {
28497        let s = parse("SHOW SUBSCRIPTIONS");
28498        assert!(matches!(s, Statement::ShowSubscriptions));
28499    }
28500
28501    #[test]
28502    fn parser_recognises_wait_for_wal_position_no_timeout() {
28503        let s = parse("WAIT FOR WAL POSITION 12345");
28504        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28505            panic!("expected WaitForWalPosition, got {s:?}")
28506        };
28507        assert_eq!(pos, 12345);
28508        assert!(timeout_ms.is_none());
28509    }
28510
28511    #[test]
28512    fn parser_recognises_wait_for_wal_position_with_timeout() {
28513        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28514        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28515            panic!()
28516        };
28517        assert_eq!(pos, 67890);
28518        assert_eq!(timeout_ms, Some(5000));
28519    }
28520
28521    #[test]
28522    fn parser_rejects_wait_with_negative_position() {
28523        // The lexer treats `-` as a token; `expect_u64_literal`
28524        // only sees the Integer that follows, so the negative
28525        // arrives as a unary-minus expression at higher levels.
28526        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28527        // parse error one way or another.
28528        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28529        assert!(!err.message.is_empty());
28530    }
28531
28532    #[test]
28533    fn parser_recognises_bare_analyze() {
28534        let s = parse("ANALYZE");
28535        assert!(matches!(s, Statement::Analyze(None)));
28536    }
28537
28538    #[test]
28539    fn parser_recognises_analyze_with_table() {
28540        let s = parse("ANALYZE users");
28541        let Statement::Analyze(Some(name)) = s else {
28542            panic!("expected Analyze, got {s:?}")
28543        };
28544        assert_eq!(name, "users");
28545    }
28546
28547    #[test]
28548    fn parser_recognises_analyze_with_quoted_table() {
28549        let s = parse("ANALYZE \"Mixed Case\"");
28550        let Statement::Analyze(Some(name)) = s else {
28551            panic!()
28552        };
28553        assert_eq!(name, "Mixed Case");
28554    }
28555
28556    #[test]
28557    fn parser_rejects_analyze_with_garbage_token() {
28558        let err = parse_statement("ANALYZE 42").expect_err("must error");
28559        assert!(!err.message.is_empty());
28560    }
28561
28562    #[test]
28563    fn analyze_display_roundtrips() {
28564        for sql in ["ANALYZE", "ANALYZE users"] {
28565            let s = parse(sql);
28566            let printed = s.to_string();
28567            let again = parse_statement(&printed)
28568                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28569            assert_eq!(s, again);
28570        }
28571    }
28572
28573    #[test]
28574    fn wait_for_display_roundtrips() {
28575        for sql in [
28576            "WAIT FOR WAL POSITION 12345",
28577            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28578        ] {
28579            let s = parse(sql);
28580            let printed = s.to_string();
28581            let again = parse_statement(&printed)
28582                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28583            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28584        }
28585    }
28586
28587    #[test]
28588    fn subscription_ddl_display_roundtrips() {
28589        for sql in [
28590            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28591            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28592            "DROP SUBSCRIPTION sub_a",
28593            "SHOW SUBSCRIPTIONS",
28594        ] {
28595            let s = parse(sql);
28596            let printed = s.to_string();
28597            let again = parse_statement(&printed)
28598                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28599            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28600        }
28601    }
28602
28603    #[test]
28604    fn parser_drop_dispatches_user_vs_publication() {
28605        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28606        // tokenises DROP. Both targets must still parse.
28607        let s = parse("DROP USER 'alice'");
28608        let Statement::DropUser { name, .. } = s else {
28609            panic!("expected DropUser, got {s:?}")
28610        };
28611        assert_eq!(name, "alice");
28612        // And DROP PUBLICATION lands the new variant.
28613        let s = parse("DROP PUBLICATION p1");
28614        assert!(matches!(s, Statement::DropPublication { .. }));
28615    }
28616
28617    #[test]
28618    fn publication_ddl_display_roundtrips() {
28619        // Every CREATE PUBLICATION variant must Display → parse →
28620        // same AST. v6.1.3 covers all three scope shapes.
28621        for sql in [
28622            "CREATE PUBLICATION pub_a",
28623            "CREATE PUBLICATION pub_a FOR ALL TABLES",
28624            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28625            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28626            "DROP PUBLICATION pub_a",
28627            "SHOW PUBLICATIONS",
28628        ] {
28629            let s = parse(sql);
28630            let printed = s.to_string();
28631            let again = parse_statement(&printed)
28632                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28633            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28634        }
28635    }
28636
28637    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28638
28639    #[test]
28640    fn create_function_returns_trigger_plpgsql_minimal() {
28641        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28642        let s = parse(sql);
28643        let Statement::CreateFunction(f) = s else {
28644            panic!("expected CreateFunction");
28645        };
28646        assert_eq!(f.name, "noop");
28647        assert!(!f.or_replace);
28648        assert!(f.args.is_empty());
28649        assert!(matches!(f.returns, FunctionReturn::Trigger));
28650        assert_eq!(f.language, "plpgsql");
28651        let FunctionBody::PlPgSql(block) = f.body else {
28652            panic!("expected PlPgSql body");
28653        };
28654        assert_eq!(block.statements.len(), 1);
28655        assert!(matches!(
28656            block.statements[0],
28657            PlPgSqlStmt::Return(ReturnTarget::New)
28658        ));
28659    }
28660
28661    #[test]
28662    fn create_function_or_replace_with_assignment() {
28663        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28664        // RETURN NEW.
28665        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28666BEGIN
28667  NEW.search_vector := to_tsvector('english', NEW.subject);
28668  RETURN NEW;
28669END;
28670$$";
28671        let s = parse(sql);
28672        let Statement::CreateFunction(f) = s else {
28673            panic!("expected CreateFunction");
28674        };
28675        assert!(f.or_replace);
28676        let FunctionBody::PlPgSql(block) = &f.body else {
28677            panic!("expected PlPgSql body");
28678        };
28679        assert_eq!(block.statements.len(), 2);
28680        // First statement: NEW.search_vector := to_tsvector(...)
28681        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28682            panic!("expected Assign as first stmt");
28683        };
28684        match target {
28685            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28686            other => panic!("expected NEW.col, got {other:?}"),
28687        }
28688        // Second statement: RETURN NEW
28689        assert!(matches!(
28690            block.statements[1],
28691            PlPgSqlStmt::Return(ReturnTarget::New)
28692        ));
28693    }
28694
28695    #[test]
28696    fn create_trigger_after_insert_or_update() {
28697        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28698        let s = parse(sql);
28699        let Statement::CreateTrigger(t) = s else {
28700            panic!("expected CreateTrigger");
28701        };
28702        assert_eq!(t.name, "tg");
28703        assert_eq!(t.table, "messages");
28704        assert_eq!(t.timing, TriggerTiming::After);
28705        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28706        assert_eq!(t.for_each, TriggerForEach::Row);
28707        assert_eq!(t.function, "update_sv");
28708    }
28709
28710    #[test]
28711    fn create_trigger_before_delete_execute_procedure_alias() {
28712        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28713        let sql =
28714            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28715        let s = parse(sql);
28716        let Statement::CreateTrigger(t) = s else {
28717            panic!("expected CreateTrigger");
28718        };
28719        assert_eq!(t.timing, TriggerTiming::Before);
28720        assert_eq!(t.events, vec![TriggerEvent::Delete]);
28721    }
28722
28723    #[test]
28724    fn drop_trigger_if_exists_round_trips() {
28725        // No parser support for DROP TRIGGER yet — added in v7.12.5
28726        // alongside the broader DROP …{IF EXISTS} cleanup. The
28727        // AST + Display impls are in place so we round-trip via
28728        // construction:
28729        let s = Statement::DropTrigger {
28730            name: "tg".into(),
28731            table: "messages".into(),
28732            if_exists: true,
28733        };
28734        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28735    }
28736
28737    #[test]
28738    fn trigger_ddl_display_roundtrips_through_parser() {
28739        // CREATE TRIGGER + its referenced CREATE FUNCTION must
28740        // Display → parse → same AST (modulo PL/pgSQL body
28741        // formatting which is parser-canonicalised).
28742        for sql in [
28743            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28744            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28745        ] {
28746            let s = parse(sql);
28747            let printed = s.to_string();
28748            let again = parse_statement(&printed)
28749                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28750            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28751        }
28752    }
28753}